@agentshouse/mdruntime 0.1.0-c52f87bb83a70 → 0.1.0-c55c6f69061ee
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 +11 -9
- package/dist/adapter.d.ts +94 -63
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attached-model.d.ts +2 -1
- package/dist/attached-model.d.ts.map +1 -1
- package/dist/attached-model.js +28 -4
- package/dist/binary.d.ts +0 -5
- package/dist/binary.d.ts.map +1 -1
- package/dist/binary.js +1 -1
- package/dist/bootstrap.d.ts +16 -8
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +142 -94
- package/dist/change-set.d.ts +1 -7
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +1 -4
- package/dist/client.d.ts +10 -11
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +54 -8
- package/dist/collection.d.ts +5 -3
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +24 -19
- package/dist/compile.d.ts +7 -2
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +125 -48
- package/dist/connection.d.ts +10 -10
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1611 -1313
- package/dist/context.d.ts +2 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/{chat/context.js → context.js} +1 -4
- package/dist/conversation.d.ts +17 -14
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +262 -159
- package/dist/durability.d.ts +1 -5
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +5 -0
- package/dist/entry.d.ts +1 -0
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +2 -1
- package/dist/file.d.ts +2 -11
- package/dist/file.d.ts.map +1 -1
- package/dist/file.js +6 -4
- package/dist/host.d.ts +4 -7
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +52 -32
- package/dist/identity.d.ts +0 -1
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +0 -3
- package/dist/index.d.ts +13 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/job.d.ts +14 -0
- package/dist/job.d.ts.map +1 -0
- package/dist/job.js +28 -0
- package/dist/live.d.ts +2 -1
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +9 -5
- 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 +10 -6
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +177 -100
- package/dist/messages.d.ts +28 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +89 -0
- package/dist/model.d.ts +6 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +7 -10
- package/dist/observation.d.ts +3 -2
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +8 -3
- package/dist/outcome.d.ts +13 -6
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +16 -5
- package/dist/overlay.js +1 -1
- package/dist/paths.d.ts +1 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +4 -0
- package/dist/phrase.d.ts +3 -0
- package/dist/phrase.d.ts.map +1 -0
- package/dist/phrase.js +8 -0
- package/dist/query.d.ts +14 -1
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +32 -2
- package/dist/reach.d.ts +13 -7
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +80 -64
- package/dist/react.d.ts +1 -0
- package/dist/react.d.ts.map +1 -1
- package/dist/readiness.d.ts +14 -17
- package/dist/readiness.d.ts.map +1 -1
- package/dist/readiness.js +2 -8
- package/dist/recovery.d.ts +1 -0
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +26 -0
- package/dist/refresh.d.ts +10 -4
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +104 -30
- package/dist/replica.d.ts +5 -5
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +7 -7
- package/dist/room-draft.d.ts +20 -3
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +131 -51
- package/dist/rows.d.ts +17 -7
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +120 -87
- package/dist/save.d.ts +2 -0
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +13 -7
- package/dist/search.d.ts +12 -95
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +10 -349
- package/dist/shared.d.ts +0 -1
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +0 -10
- package/dist/status.d.ts +4 -3
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +3 -1
- package/dist/store.d.ts +34 -60
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +71 -275
- package/dist/stored.d.ts +42 -23
- package/dist/stored.d.ts.map +1 -1
- package/dist/stored.js +198 -210
- package/dist/tables.d.ts +79 -46
- package/dist/tables.d.ts.map +1 -1
- package/dist/tables.js +101 -53
- package/dist/transfer.d.ts +0 -5
- package/dist/transfer.d.ts.map +1 -1
- package/dist/validation.d.ts +3 -4
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +35 -96
- package/dist/wire.d.ts +6 -9
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +3 -3
- package/package.json +14 -18
- package/AGENTS.md +0 -310
- package/dist/chat/acts.d.ts +0 -33
- package/dist/chat/acts.d.ts.map +0 -1
- package/dist/chat/acts.js +0 -36
- package/dist/chat/context.d.ts +0 -3
- package/dist/chat/context.d.ts.map +0 -1
- package/dist/chat/earlier.d.ts +0 -10
- package/dist/chat/earlier.d.ts.map +0 -1
- package/dist/chat/earlier.js +0 -54
- package/dist/chat/house.d.ts +0 -25
- package/dist/chat/house.d.ts.map +0 -1
- package/dist/chat/house.js +0 -38
- package/dist/chat/input.d.ts +0 -11
- package/dist/chat/input.d.ts.map +0 -1
- package/dist/chat/input.js +0 -92
- package/dist/chat/kit.d.ts +0 -61
- package/dist/chat/kit.d.ts.map +0 -1
- package/dist/chat/kit.js +0 -123
- package/dist/chat/lasting.d.ts +0 -6
- package/dist/chat/lasting.d.ts.map +0 -1
- package/dist/chat/lasting.js +0 -19
- package/dist/chat/list.d.ts +0 -28
- package/dist/chat/list.d.ts.map +0 -1
- package/dist/chat/list.js +0 -103
- package/dist/chat/markdown.d.ts +0 -21
- package/dist/chat/markdown.d.ts.map +0 -1
- package/dist/chat/markdown.js +0 -246
- package/dist/chat/message.d.ts +0 -7
- package/dist/chat/message.d.ts.map +0 -1
- package/dist/chat/message.js +0 -237
- package/dist/chat/messages.d.ts +0 -29
- package/dist/chat/messages.d.ts.map +0 -1
- package/dist/chat/messages.js +0 -190
- package/dist/chat/name.d.ts +0 -7
- package/dist/chat/name.d.ts.map +0 -1
- package/dist/chat/name.js +0 -37
- package/dist/chat/opened.d.ts +0 -12
- package/dist/chat/opened.d.ts.map +0 -1
- package/dist/chat/opened.js +0 -19
- package/dist/chat/overlay.d.ts +0 -32
- package/dist/chat/overlay.d.ts.map +0 -1
- package/dist/chat/overlay.js +0 -103
- package/dist/chat/parts.d.ts +0 -58
- package/dist/chat/parts.d.ts.map +0 -1
- package/dist/chat/parts.js +0 -120
- package/dist/chat/pieces.d.ts +0 -62
- package/dist/chat/pieces.d.ts.map +0 -1
- package/dist/chat/pieces.js +0 -1110
- package/dist/chat/process.d.ts +0 -20
- package/dist/chat/process.d.ts.map +0 -1
- package/dist/chat/process.js +0 -32
- package/dist/chat/provider.d.ts +0 -68
- package/dist/chat/provider.d.ts.map +0 -1
- package/dist/chat/provider.js +0 -619
- package/dist/chat/question.d.ts +0 -9
- package/dist/chat/question.d.ts.map +0 -1
- package/dist/chat/question.js +0 -69
- package/dist/chat/questions.d.ts +0 -31
- package/dist/chat/questions.d.ts.map +0 -1
- package/dist/chat/questions.js +0 -111
- package/dist/chat/read.d.ts +0 -5
- package/dist/chat/read.d.ts.map +0 -1
- package/dist/chat/read.js +0 -13
- package/dist/chat/ready.d.ts +0 -37
- package/dist/chat/ready.d.ts.map +0 -1
- package/dist/chat/ready.js +0 -93
- package/dist/chat/room-file.d.ts +0 -8
- package/dist/chat/room-file.d.ts.map +0 -1
- package/dist/chat/room-file.js +0 -72
- package/dist/chat/steady.d.ts +0 -3
- package/dist/chat/steady.d.ts.map +0 -1
- package/dist/chat/steady.js +0 -26
- package/dist/chat/style.d.ts +0 -4
- package/dist/chat/style.d.ts.map +0 -1
- package/dist/chat/style.js +0 -306
- package/dist/chat.d.ts +0 -15
- package/dist/chat.d.ts.map +0 -1
- package/dist/chat.js +0 -8
- package/dist/reads.d.ts +0 -11
- package/dist/reads.d.ts.map +0 -1
- package/dist/reads.js +0 -98
package/dist/connection.js
CHANGED
|
@@ -1,51 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { attachedModel,
|
|
1
|
+
import { parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
|
|
2
|
+
import { attachedModel, builtInModel, roomTypesOf } from './attached-model.js';
|
|
3
3
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
4
4
|
import { observing, whole } from './observation.js';
|
|
5
|
-
import {
|
|
5
|
+
import { bodied, identityOf, planOf, plain, queryKey, } from './query.js';
|
|
6
6
|
import { createLiveQueries } from './live.js';
|
|
7
7
|
import { seededResult } from './seed.js';
|
|
8
8
|
import { documentKey, roomOfKey } from './rows.js';
|
|
9
|
-
import {
|
|
9
|
+
import { searchPlanOf } from './search.js';
|
|
10
10
|
import { capped, MAXIMUM_WINDOW } from './windows.js';
|
|
11
11
|
import { bootstrapRoom } from './bootstrap.js';
|
|
12
12
|
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
13
13
|
import { createConversationStore, } from './conversation.js';
|
|
14
14
|
import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
|
|
15
|
-
import { available, collectionOf,
|
|
15
|
+
import { available, collectionOf, composedOf, validRoot, } from './collection.js';
|
|
16
16
|
import { runtimeError } from './failure.js';
|
|
17
17
|
import { createLog } from './log.js';
|
|
18
18
|
import { binaryRecord } from './binary.js';
|
|
19
|
+
import { attachedFile } from './file.js';
|
|
19
20
|
import { openedFiles } from './opened.js';
|
|
20
|
-
import { ABSENT_TARGET, absentBinary,
|
|
21
|
+
import { ABSENT_TARGET, absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
|
|
21
22
|
import { createRecovery } from './recovery.js';
|
|
22
|
-
import { doorReads } from './reads.js';
|
|
23
23
|
import { advancement } from './refresh.js';
|
|
24
24
|
import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
|
|
25
|
-
import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths,
|
|
26
|
-
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
25
|
+
import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
|
|
26
|
+
import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, readSavedParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
27
27
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
28
|
-
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
29
|
-
import { readinessOf,
|
|
28
|
+
import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
|
|
29
|
+
import { readinessOf, } from './readiness.js';
|
|
30
30
|
import { FencedMap } from './fence.js';
|
|
31
|
-
import { fingerprint } from './stored.js';
|
|
32
31
|
import { sameBinary } from './overlay.js';
|
|
33
32
|
import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
34
33
|
import { identityTime, roomDraftRevision } from './identity.js';
|
|
35
|
-
import { beneath, normalize } from './paths.js';
|
|
34
|
+
import { ancestors, beneath, normalize } from './paths.js';
|
|
36
35
|
import { openRuntimeStore, } from './store.js';
|
|
37
|
-
import {
|
|
36
|
+
import { validatedNeighbourhood } from './validation.js';
|
|
38
37
|
import { unconfirmed } from './transfer.js';
|
|
39
38
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
40
|
-
function roomsAlone(plan) {
|
|
41
|
-
return (plan.reach === null &&
|
|
42
|
-
plan.root.types === null &&
|
|
43
|
-
plan.root.where === null &&
|
|
44
|
-
plan.root.pathPrefix === null &&
|
|
45
|
-
plan.paths === null &&
|
|
46
|
-
plan.hop === null &&
|
|
47
|
-
plan.backlinks === null);
|
|
48
|
-
}
|
|
49
39
|
function within(root, path) {
|
|
50
40
|
return root === '' || beneath(root, path);
|
|
51
41
|
}
|
|
@@ -63,39 +53,47 @@ const OPENING_LANE = 'opening';
|
|
|
63
53
|
const CONVERSATION_LANE = 'conversations';
|
|
64
54
|
const ROOM_READS = 256;
|
|
65
55
|
const ROOM_ATTEMPTS = 16;
|
|
66
|
-
const SEARCH_BATCH = 256;
|
|
67
56
|
const ERASE_BATCH = 512;
|
|
68
|
-
const
|
|
69
|
-
const
|
|
57
|
+
const DERIVE_BYTES = 1 << 23;
|
|
58
|
+
const DERIVE_FLOOR = 1 << 18;
|
|
59
|
+
// An index build aims at this much database work, so a Save, a query or the next part's progress waits behind at most one short build.
|
|
60
|
+
const DERIVE_AIM_MS = 250;
|
|
61
|
+
// A build past its floor of text stops after a second and a half of work, its full-text writes included, so a Room whose text costs ten times the usual holds a Save, a query or the next part's progress behind no longer build.
|
|
62
|
+
const DERIVE_LONGEST_MS = 1_500;
|
|
70
63
|
const QUERY_BYTES = 1 << 20;
|
|
71
64
|
const STAGE_BYTES = 1 << 20;
|
|
72
|
-
const
|
|
65
|
+
const STAGE_FLOOR = 1 << 16;
|
|
66
|
+
// A batch aims at this much database work, so a Save or a query waits behind at most one short write.
|
|
67
|
+
const STAGE_AIM_MS = 50;
|
|
68
|
+
const STREAMS_HELD = 4;
|
|
69
|
+
const NOTHING_HELD = { bytes: 0 };
|
|
70
|
+
const QUEUED_TEXT = 1 << 23;
|
|
71
|
+
// Bytes still arriving are reported at most this often, so readiness is not rebuilt for every chunk the network hands over while a slow link still moves the figure.
|
|
72
|
+
const ARRIVAL_REPORT_MS = 250;
|
|
73
73
|
// 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.
|
|
74
74
|
const LOCK_GRACE_MS = 120_000;
|
|
75
75
|
function operationOf(input) {
|
|
76
76
|
const operation = input?.operation_id;
|
|
77
77
|
return typeof operation === 'string' ? operation : null;
|
|
78
78
|
}
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
function readUntil(read, signal) {
|
|
80
|
+
if (signal.aborted)
|
|
81
|
+
return Promise.resolve({ kind: 'transport-uncertainty' });
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const stop = () => resolve({ kind: 'transport-uncertainty' });
|
|
84
|
+
signal.addEventListener('abort', stop, { once: true });
|
|
85
|
+
read.then(resolve, reject).finally(() => signal.removeEventListener('abort', stop));
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
|
-
|
|
88
|
-
return `room ${locator}`;
|
|
89
|
-
}
|
|
88
|
+
const TEXT_READS = 4;
|
|
90
89
|
function pendingKey(locator, id) {
|
|
91
90
|
return `${locator}\u0000${id}`;
|
|
92
91
|
}
|
|
93
92
|
export function composeRuntime(options) {
|
|
94
93
|
const localSaves = options.adapter.localSaves === true;
|
|
95
|
-
const roomTypes = roomTypesOf(options.adapter.roomTypes);
|
|
94
|
+
const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
|
|
96
95
|
let authorization = options.authorization;
|
|
97
96
|
let opening;
|
|
98
|
-
let store;
|
|
99
97
|
const fences = new Map();
|
|
100
98
|
const isFenced = (locator) => fences.has(locator);
|
|
101
99
|
let fencings = 0;
|
|
@@ -103,44 +101,52 @@ export function composeRuntime(options) {
|
|
|
103
101
|
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
104
102
|
let discoveries = 0;
|
|
105
103
|
const replicas = new FencedMap(isFenced);
|
|
104
|
+
const reopened = new Set();
|
|
105
|
+
const usableWhenHeld = options.usableWhenHeld ?? [];
|
|
106
106
|
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
107
107
|
const pendings = new Map();
|
|
108
|
+
const landedAhead = new Map();
|
|
108
109
|
const seeds = new Map();
|
|
109
110
|
const seedWatchers = new Map();
|
|
110
111
|
const roomDrafts = new FencedMap(isFenced);
|
|
112
|
+
const attachedFiles = new FencedMap(isFenced);
|
|
111
113
|
const presentations = new Map();
|
|
112
114
|
let limits = [];
|
|
113
115
|
let capabilities = [];
|
|
114
116
|
const rooms = new FencedMap(isFenced);
|
|
115
117
|
const counted = new Map();
|
|
116
118
|
const unreadable = new Set();
|
|
117
|
-
const
|
|
118
|
-
const models = new FencedMap(
|
|
119
|
-
const liveModels = new FencedMap(
|
|
119
|
+
const builtIn = builtInModel(roomTypes);
|
|
120
|
+
const models = new FencedMap(isFenced);
|
|
121
|
+
const liveModels = new FencedMap(isFenced);
|
|
120
122
|
const invalidModels = new Set();
|
|
121
123
|
const discovered = new Map();
|
|
122
|
-
|
|
124
|
+
let inFlight = new AbortController();
|
|
125
|
+
const refreshed = options.adapter.refresh?.bind(options.adapter);
|
|
126
|
+
const reads = {
|
|
127
|
+
bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
|
|
128
|
+
...(refreshed === undefined
|
|
129
|
+
? {}
|
|
130
|
+
: {
|
|
131
|
+
refresh: (authorized, request) => readUntil(refreshed(authorized, request), inFlight.signal),
|
|
132
|
+
}),
|
|
133
|
+
reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
|
|
134
|
+
};
|
|
123
135
|
let roomsListed = false;
|
|
124
136
|
const scopedRefusals = new Map();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
function modelKey(locator, attached) {
|
|
128
|
-
return `${attached} ${locator}`;
|
|
137
|
+
function modelOf(locator) {
|
|
138
|
+
return models.get(locator) ?? NO_MODEL;
|
|
129
139
|
}
|
|
130
|
-
function
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
function savedModel(locator, attached) {
|
|
134
|
-
return invalidModels.has(modelKey(locator, attached))
|
|
140
|
+
function savedModel(locator) {
|
|
141
|
+
return invalidModels.has(locator)
|
|
135
142
|
? { status: 'unavailable', reason: 'revoked' }
|
|
136
|
-
: modelOf(locator
|
|
143
|
+
: modelOf(locator);
|
|
144
|
+
}
|
|
145
|
+
function composed(record) {
|
|
146
|
+
return compositionOf(record, builtIn);
|
|
137
147
|
}
|
|
138
148
|
function compositionFor(record) {
|
|
139
|
-
return
|
|
140
|
-
...record,
|
|
141
|
-
model: savedModel(record.locator, 'collection'),
|
|
142
|
-
selectionModel: savedModel(record.locator, 'typed-selection'),
|
|
143
|
-
});
|
|
149
|
+
return composed({ ...record, model: savedModel(record.locator) });
|
|
144
150
|
}
|
|
145
151
|
let refusal = null;
|
|
146
152
|
let storageLoss = null;
|
|
@@ -150,10 +156,10 @@ export function composeRuntime(options) {
|
|
|
150
156
|
let closed = false;
|
|
151
157
|
let stopping = false;
|
|
152
158
|
let withdrawStream;
|
|
153
|
-
const announced = new Map();
|
|
154
159
|
let underway = null;
|
|
155
160
|
let streamed = null;
|
|
156
161
|
let handed = false;
|
|
162
|
+
let naming = 0;
|
|
157
163
|
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
158
164
|
const repairing = new Set();
|
|
159
165
|
const calling = new Set();
|
|
@@ -164,15 +170,18 @@ export function composeRuntime(options) {
|
|
|
164
170
|
let recorded = false;
|
|
165
171
|
const stale = new Set();
|
|
166
172
|
let generation = 0;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
const reloadsDue = new Map();
|
|
173
|
+
const reloadsDue = new Set();
|
|
174
|
+
const highest = new Map();
|
|
175
|
+
const beyondHorizon = new Set();
|
|
171
176
|
const lanes = new Map();
|
|
172
|
-
|
|
177
|
+
const roomLanes = new Map();
|
|
178
|
+
let roomWork = 0;
|
|
179
|
+
const roomReads = { free: ROOM_READS, waiting: [] };
|
|
173
180
|
const working = new Set();
|
|
174
181
|
let mutations = Promise.resolve();
|
|
182
|
+
let gathering = null;
|
|
175
183
|
const roomListeners = new Set();
|
|
184
|
+
const unlistedRooms = new Set();
|
|
176
185
|
const refusalListeners = new Set();
|
|
177
186
|
const storageLossListeners = new Set();
|
|
178
187
|
const statusListeners = new Set();
|
|
@@ -183,6 +192,8 @@ export function composeRuntime(options) {
|
|
|
183
192
|
let withdrawConversations;
|
|
184
193
|
let currencyQueued = null;
|
|
185
194
|
let socketOpen = false;
|
|
195
|
+
let announced = false;
|
|
196
|
+
let missedChange = false;
|
|
186
197
|
const confirmed = new Set();
|
|
187
198
|
let lockGrace = null;
|
|
188
199
|
const lockListeners = new Set();
|
|
@@ -192,73 +203,84 @@ export function composeRuntime(options) {
|
|
|
192
203
|
const serviceModeListeners = new Set();
|
|
193
204
|
let member = null;
|
|
194
205
|
const memberListeners = new Set();
|
|
195
|
-
|
|
206
|
+
let authorityChanges = 0;
|
|
207
|
+
const authorityChangeListeners = new Set();
|
|
196
208
|
const unpreparedDemand = new Set();
|
|
197
|
-
const
|
|
209
|
+
const unpreparable = new Set();
|
|
198
210
|
let preparing = false;
|
|
199
211
|
let targets = {};
|
|
200
212
|
let persistence = options.shell.persistence;
|
|
201
|
-
let
|
|
202
|
-
|
|
203
|
-
let
|
|
213
|
+
let full = false;
|
|
214
|
+
let probing = null;
|
|
215
|
+
let testing = null;
|
|
216
|
+
const database = {
|
|
217
|
+
async ask(request) {
|
|
218
|
+
try {
|
|
219
|
+
return await options.database.ask(request);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (refusedForSpace(error))
|
|
223
|
+
filled();
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
};
|
|
204
228
|
const readinessWatches = new Set();
|
|
205
229
|
let readinessQueued = false;
|
|
206
230
|
const catching = new Set();
|
|
207
231
|
const transfers = new FencedMap(isFenced);
|
|
232
|
+
const installs = new Set();
|
|
233
|
+
const arrivals = new Map();
|
|
234
|
+
const streaming = new Set();
|
|
235
|
+
const underived = new Map();
|
|
236
|
+
const unindexed = new Map();
|
|
237
|
+
const startedOver = new Map();
|
|
238
|
+
let arrivalReported = 0;
|
|
239
|
+
let streamsFree = STREAMS_HELD;
|
|
240
|
+
let queuedText = 0;
|
|
241
|
+
const installWaiters = [];
|
|
242
|
+
const stageBudget = {
|
|
243
|
+
bytes: STAGE_FLOOR * 4,
|
|
244
|
+
floor: STAGE_FLOOR,
|
|
245
|
+
ceiling: STAGE_BYTES,
|
|
246
|
+
aim: STAGE_AIM_MS,
|
|
247
|
+
};
|
|
248
|
+
const deriveBudget = {
|
|
249
|
+
bytes: DERIVE_FLOOR * 4,
|
|
250
|
+
floor: DERIVE_FLOOR,
|
|
251
|
+
ceiling: DERIVE_BYTES,
|
|
252
|
+
aim: DERIVE_AIM_MS,
|
|
253
|
+
};
|
|
254
|
+
const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
|
|
208
255
|
const conversations = createConversationStore({
|
|
209
|
-
agents: declaredAgents,
|
|
210
|
-
documents: (declaration, after) => rowsQuery(planOf({
|
|
211
|
-
rooms: [declaration.locator],
|
|
212
|
-
types: [declaration.type],
|
|
213
|
-
select: {
|
|
214
|
-
fields: [
|
|
215
|
-
declaration.identity,
|
|
216
|
-
declaration.name,
|
|
217
|
-
...(declaration.retired === undefined ? [] : [declaration.retired]),
|
|
218
|
-
].map((field) => `/${escapePointer(field)}`),
|
|
219
|
-
},
|
|
220
|
-
limit: MAXIMUM_WINDOW,
|
|
221
|
-
...(after === undefined ? {} : { after }),
|
|
222
|
-
}, roomFacts())),
|
|
223
256
|
call: (name, input) => invokeAction(name, input),
|
|
257
|
+
history: (request) => readConversationHistory(request),
|
|
224
258
|
durably,
|
|
225
259
|
stream: () => options.adapter.conversations,
|
|
226
260
|
publish: publishChangeSet,
|
|
227
261
|
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
262
|
+
full: () => full,
|
|
228
263
|
closed: () => closed,
|
|
229
264
|
unanswered: resendLater,
|
|
230
265
|
expired: (operation) => pastHorizon(operation),
|
|
231
266
|
});
|
|
232
|
-
const searchIndex = createSearchIndex({
|
|
233
|
-
async *room(room) {
|
|
234
|
-
const local = await openLocal();
|
|
235
|
-
for (let after = null;;) {
|
|
236
|
-
const key = replicas.get(room)?.record.key;
|
|
237
|
-
if (key === undefined)
|
|
238
|
-
return;
|
|
239
|
-
const page = await local.searched(key, room, after, SEARCH_BATCH, null);
|
|
240
|
-
yield page.documents;
|
|
241
|
-
if (page.next === null)
|
|
242
|
-
return;
|
|
243
|
-
after = page.next;
|
|
244
|
-
}
|
|
245
|
-
},
|
|
246
|
-
async documents(room, paths) {
|
|
247
|
-
const key = replicas.get(room)?.record.key;
|
|
248
|
-
if (key === undefined || paths.length === 0)
|
|
249
|
-
return [];
|
|
250
|
-
return (await (await openLocal()).searched(key, room, null, paths.length, paths)).documents;
|
|
251
|
-
},
|
|
252
|
-
});
|
|
253
|
-
const roomsToIndex = new Set();
|
|
254
|
-
const conversationsToIndex = new Set();
|
|
255
|
-
let indexing = false;
|
|
256
267
|
const journal = createLog();
|
|
257
|
-
const keptFiles = openedFiles(
|
|
258
|
-
|
|
268
|
+
const keptFiles = openedFiles(database, isFenced);
|
|
269
|
+
const holding = new Map();
|
|
270
|
+
const unholdable = new Map();
|
|
271
|
+
let readyFromStore = () => { };
|
|
259
272
|
const ready = new Promise((resolve) => {
|
|
260
|
-
|
|
273
|
+
readyFromStore = resolve;
|
|
261
274
|
});
|
|
275
|
+
let notifyDiscovered = () => { };
|
|
276
|
+
const firstDiscovery = new Promise((resolve) => {
|
|
277
|
+
notifyDiscovered = resolve;
|
|
278
|
+
});
|
|
279
|
+
let roomsNamed = false;
|
|
280
|
+
function notifyReady() {
|
|
281
|
+
notifyDiscovered();
|
|
282
|
+
readyFromStore();
|
|
283
|
+
}
|
|
262
284
|
function rememberRoomDraft(record) {
|
|
263
285
|
const room = roomDrafts.get(record.locator) ?? new Map();
|
|
264
286
|
room.set(record.path, record);
|
|
@@ -278,7 +300,7 @@ export function composeRuntime(options) {
|
|
|
278
300
|
return undefined;
|
|
279
301
|
const local = await openLocal();
|
|
280
302
|
const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
|
|
281
|
-
return { ...slice, collection: collectionOf(slice.documents,
|
|
303
|
+
return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
|
|
282
304
|
}
|
|
283
305
|
function roomDraftOf(record, visible) {
|
|
284
306
|
return structuredClone({
|
|
@@ -334,14 +356,40 @@ export function composeRuntime(options) {
|
|
|
334
356
|
const value = presentations.get(app);
|
|
335
357
|
return value === undefined ? null : structuredClone(value);
|
|
336
358
|
}
|
|
337
|
-
function
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
359
|
+
async function writtenRoomDraft(room, path, revision, input, next) {
|
|
360
|
+
const parts = readParts(input, path);
|
|
361
|
+
if (path === '' || normalize(path) !== path)
|
|
362
|
+
throw runtimeError('room-draft-malformed', `room-draft-malformed at ${path}: a path inside the Room, such as notes/a.md`);
|
|
363
|
+
const local = await openLocal();
|
|
364
|
+
const current = roomDrafts.get(room)?.get(path);
|
|
365
|
+
if ((current?.revision ?? null) !== revision) {
|
|
366
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
367
|
+
}
|
|
368
|
+
const file = parts.file === undefined
|
|
369
|
+
? undefined
|
|
370
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined
|
|
371
|
+
? binaryBase(await visibleOf(room, [path]), path)
|
|
372
|
+
: parts.file.base);
|
|
373
|
+
const record = {
|
|
374
|
+
locator: room,
|
|
375
|
+
path,
|
|
376
|
+
revision: next,
|
|
377
|
+
parts: held(current?.parts, parts, file),
|
|
378
|
+
returned: current?.returned ?? null,
|
|
379
|
+
};
|
|
380
|
+
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
381
|
+
? []
|
|
382
|
+
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
383
|
+
rememberRoomDraft(record);
|
|
384
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
385
|
+
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
386
|
+
}
|
|
387
|
+
async function keptPresentation(app, next) {
|
|
388
|
+
const local = await openLocal();
|
|
389
|
+
const stored = next(presentations.get(app));
|
|
390
|
+
await local.present({ app, value: stored });
|
|
391
|
+
presentations.set(app, stored);
|
|
392
|
+
publishChangeSet(changeSet('', { presentation: app }));
|
|
345
393
|
}
|
|
346
394
|
function emitPresentation(app) {
|
|
347
395
|
if (closed)
|
|
@@ -356,6 +404,20 @@ export function composeRuntime(options) {
|
|
|
356
404
|
(queuesWithoutReplica.size > 0 ||
|
|
357
405
|
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
358
406
|
}
|
|
407
|
+
function unsent() {
|
|
408
|
+
const changed = new Map();
|
|
409
|
+
for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
410
|
+
for (const { batch, state } of entriesOf(room)) {
|
|
411
|
+
if (state === 'local')
|
|
412
|
+
continue;
|
|
413
|
+
for (const { path } of batch.changes)
|
|
414
|
+
changed.set(`${room}\u0000${path}`, { room, path });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return [...changed]
|
|
418
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
419
|
+
.map(([, change]) => change);
|
|
420
|
+
}
|
|
359
421
|
function attention() {
|
|
360
422
|
const attentionRequired = [];
|
|
361
423
|
for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
|
|
@@ -367,16 +429,9 @@ export function composeRuntime(options) {
|
|
|
367
429
|
returned: structuredClone(record.returned),
|
|
368
430
|
});
|
|
369
431
|
}
|
|
370
|
-
for (const [room, refusal] of sourceRefusals)
|
|
371
|
-
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
372
432
|
attentionRequired.sort((left, right) => {
|
|
373
433
|
if (left.room !== right.room)
|
|
374
434
|
return left.room < right.room ? -1 : 1;
|
|
375
|
-
if ('refusal' in left || 'refusal' in right) {
|
|
376
|
-
if ('refusal' in left && 'refusal' in right)
|
|
377
|
-
return 0;
|
|
378
|
-
return 'refusal' in left ? -1 : 1;
|
|
379
|
-
}
|
|
380
435
|
if (left.path !== right.path)
|
|
381
436
|
return left.path < right.path ? -1 : 1;
|
|
382
437
|
return 0;
|
|
@@ -388,7 +443,7 @@ export function composeRuntime(options) {
|
|
|
388
443
|
if (reset === null || submission.reset < reset)
|
|
389
444
|
reset = submission.reset;
|
|
390
445
|
}
|
|
391
|
-
return { attentionRequired, reset };
|
|
446
|
+
return { attentionRequired, unsent: unsent(), reset };
|
|
392
447
|
}
|
|
393
448
|
function documentsIn(locator) {
|
|
394
449
|
return replicas.get(locator)?.count ?? 0;
|
|
@@ -461,7 +516,7 @@ export function composeRuntime(options) {
|
|
|
461
516
|
const governed = governs(reach, room);
|
|
462
517
|
const attached = [
|
|
463
518
|
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
464
|
-
...(governed.selection ? [
|
|
519
|
+
...(governed.selection ? [builtIn] : []),
|
|
465
520
|
];
|
|
466
521
|
return {
|
|
467
522
|
locator: room.locator,
|
|
@@ -477,11 +532,26 @@ export function composeRuntime(options) {
|
|
|
477
532
|
const value = listed();
|
|
478
533
|
for (const listener of roomListeners)
|
|
479
534
|
listener(value);
|
|
535
|
+
emitKnownRooms();
|
|
536
|
+
}
|
|
537
|
+
function emitKnownRooms() {
|
|
538
|
+
conversations.learned();
|
|
539
|
+
if (unlistedRooms.size === 0)
|
|
540
|
+
return;
|
|
541
|
+
if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
|
|
542
|
+
return;
|
|
543
|
+
const value = listed();
|
|
544
|
+
for (const listener of [...unlistedRooms]) {
|
|
545
|
+
unlistedRooms.delete(listener);
|
|
546
|
+
roomListeners.add(listener);
|
|
547
|
+
listener(value);
|
|
548
|
+
}
|
|
480
549
|
}
|
|
481
550
|
function emitRefusal() {
|
|
482
551
|
queueReadiness();
|
|
483
552
|
for (const listener of refusalListeners)
|
|
484
553
|
listener(refusal);
|
|
554
|
+
emitKnownRooms();
|
|
485
555
|
}
|
|
486
556
|
function emitStorageLoss() {
|
|
487
557
|
queueReadiness();
|
|
@@ -502,6 +572,7 @@ export function composeRuntime(options) {
|
|
|
502
572
|
if (closed)
|
|
503
573
|
return;
|
|
504
574
|
queueReadiness();
|
|
575
|
+
emitKnownRooms();
|
|
505
576
|
const next = status.state === 'synced' && !conversations.current()
|
|
506
577
|
? { ...status, state: 'syncing' }
|
|
507
578
|
: status;
|
|
@@ -515,9 +586,6 @@ export function composeRuntime(options) {
|
|
|
515
586
|
if (localSaves)
|
|
516
587
|
return Object.freeze({ batch, state: 'local' });
|
|
517
588
|
const state = pendings.get(pendingKey(locator, batch.id));
|
|
518
|
-
if (state?.accepted !== undefined) {
|
|
519
|
-
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
520
|
-
}
|
|
521
589
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
522
590
|
}
|
|
523
591
|
function entriesOf(locator) {
|
|
@@ -532,23 +600,29 @@ export function composeRuntime(options) {
|
|
|
532
600
|
}
|
|
533
601
|
function applyChangeSet(set) {
|
|
534
602
|
liveQueries.touched(set);
|
|
535
|
-
searchIndex.changed(set);
|
|
536
603
|
}
|
|
537
604
|
function unpublished(error) {
|
|
538
605
|
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
539
606
|
}
|
|
607
|
+
function indexed(locator) {
|
|
608
|
+
return replicas.get(locator)?.indexed === true;
|
|
609
|
+
}
|
|
540
610
|
async function prepareReplica(locator) {
|
|
541
611
|
for (;;) {
|
|
542
612
|
const held = replicas.get(locator);
|
|
543
|
-
if (held === undefined || closed ||
|
|
613
|
+
if (held === undefined || closed || held.indexed)
|
|
544
614
|
return;
|
|
545
615
|
const key = held.record.key;
|
|
546
|
-
const done = await exclusive(async () =>
|
|
547
|
-
|
|
616
|
+
const done = await exclusive(async () => {
|
|
617
|
+
const local = await openLocal();
|
|
618
|
+
return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
|
|
619
|
+
});
|
|
620
|
+
const now = replicas.get(locator);
|
|
621
|
+
if (now?.record.key !== key)
|
|
548
622
|
continue;
|
|
549
|
-
if (!done)
|
|
623
|
+
if (!done.done)
|
|
550
624
|
continue;
|
|
551
|
-
|
|
625
|
+
replicas.set(locator, { ...now, indexed: true });
|
|
552
626
|
publishChangeSet(changeSet(locator, { documents: null }));
|
|
553
627
|
liveQueries.recover();
|
|
554
628
|
queueReadiness();
|
|
@@ -556,11 +630,11 @@ export function composeRuntime(options) {
|
|
|
556
630
|
}
|
|
557
631
|
}
|
|
558
632
|
function unprepared() {
|
|
559
|
-
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !
|
|
633
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
|
|
560
634
|
}
|
|
561
|
-
function
|
|
562
|
-
|
|
563
|
-
journal.record(`
|
|
635
|
+
function unpreparedBy(locator, error) {
|
|
636
|
+
unpreparable.add(locator);
|
|
637
|
+
journal.record(`preparation failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
564
638
|
queueReadiness();
|
|
565
639
|
}
|
|
566
640
|
function prepare(locators = []) {
|
|
@@ -576,7 +650,7 @@ export function composeRuntime(options) {
|
|
|
576
650
|
if (closed)
|
|
577
651
|
return;
|
|
578
652
|
unpreparedDemand.delete(waiting);
|
|
579
|
-
await prepareReplica(waiting).catch((error) =>
|
|
653
|
+
await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
|
|
580
654
|
}
|
|
581
655
|
}
|
|
582
656
|
finally {
|
|
@@ -617,14 +691,14 @@ export function composeRuntime(options) {
|
|
|
617
691
|
pending.push({ room: locator, awaits: 'acquisition' });
|
|
618
692
|
continue;
|
|
619
693
|
}
|
|
620
|
-
if (!
|
|
694
|
+
if (!replica.indexed) {
|
|
621
695
|
pending.push({ room: locator, awaits: 'preparation' });
|
|
622
696
|
continue;
|
|
623
697
|
}
|
|
624
698
|
coverage.push({
|
|
625
699
|
room: locator,
|
|
626
700
|
generation: replica.record.transfer,
|
|
627
|
-
current: !(catching.has(locator) ||
|
|
701
|
+
current: !(catching.has(locator) || transfers.has(locator)),
|
|
628
702
|
});
|
|
629
703
|
read.push({ locator, key: replica.record.key });
|
|
630
704
|
for (const batch of replica.queue)
|
|
@@ -650,9 +724,7 @@ export function composeRuntime(options) {
|
|
|
650
724
|
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
651
725
|
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
652
726
|
}
|
|
653
|
-
function roomState(locator
|
|
654
|
-
if (sourceRefusals.has(locator))
|
|
655
|
-
return 'refused';
|
|
727
|
+
function roomState(locator) {
|
|
656
728
|
const listedRoom = discovered.get(locator);
|
|
657
729
|
if (roomsListed &&
|
|
658
730
|
(listedRoom === undefined ||
|
|
@@ -661,27 +733,17 @@ export function composeRuntime(options) {
|
|
|
661
733
|
return 'unadmitted';
|
|
662
734
|
if (!replicas.has(locator))
|
|
663
735
|
return 'acquiring';
|
|
664
|
-
if (!
|
|
736
|
+
if (!indexed(locator))
|
|
665
737
|
return 'preparing';
|
|
666
|
-
if (search && !searchIndex.rooms().has(locator))
|
|
667
|
-
return 'indexing';
|
|
668
738
|
return 'complete';
|
|
669
739
|
}
|
|
670
|
-
function conversationCoverage(reach
|
|
740
|
+
function conversationCoverage(reach) {
|
|
671
741
|
const declared = reach === null || reach.conversations;
|
|
672
|
-
if (!declared || options.adapter.conversations === undefined
|
|
742
|
+
if (!declared || options.adapter.conversations === undefined)
|
|
673
743
|
return 'undeclared';
|
|
674
744
|
if (!conversations.known() || !conversations.current())
|
|
675
745
|
return 'acquiring';
|
|
676
|
-
|
|
677
|
-
return 'complete';
|
|
678
|
-
const unindexed = conversations
|
|
679
|
-
.searchable()
|
|
680
|
-
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
681
|
-
if (unindexed.length === 0)
|
|
682
|
-
return 'complete';
|
|
683
|
-
index([], unindexed);
|
|
684
|
-
return 'indexing';
|
|
746
|
+
return 'complete';
|
|
685
747
|
}
|
|
686
748
|
function connectionOf() {
|
|
687
749
|
if (socketOpen)
|
|
@@ -692,58 +754,77 @@ export function composeRuntime(options) {
|
|
|
692
754
|
? 'connected'
|
|
693
755
|
: 'connecting';
|
|
694
756
|
}
|
|
695
|
-
function
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
757
|
+
function bytesOf(room, underway) {
|
|
758
|
+
const transfer = transfers.get(room);
|
|
759
|
+
if (transfer !== undefined) {
|
|
760
|
+
const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
|
|
761
|
+
return {
|
|
762
|
+
done: transfer.done.bytes - Math.ceil(owed),
|
|
763
|
+
total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
|
|
767
|
+
return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
|
|
768
|
+
}
|
|
769
|
+
function installationOf(coverage) {
|
|
770
|
+
const rooms = { done: 0, total: 0 };
|
|
771
|
+
const bytes = { done: 0, total: 0 };
|
|
772
|
+
let installing = 0;
|
|
773
|
+
let downloading = false;
|
|
774
|
+
for (const { room, state } of coverage) {
|
|
775
|
+
if (state === 'unadmitted')
|
|
776
|
+
continue;
|
|
777
|
+
rooms.total += 1;
|
|
778
|
+
if (state === 'complete')
|
|
779
|
+
rooms.done += 1;
|
|
780
|
+
const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
|
|
781
|
+
if (underway)
|
|
782
|
+
installing += 1;
|
|
783
|
+
if (underway && (streaming.has(room) || !arrivals.has(room)))
|
|
784
|
+
downloading = true;
|
|
785
|
+
const now = bytesOf(room, underway);
|
|
786
|
+
const earlier = underway ? startedOver.get(room) : undefined;
|
|
787
|
+
bytes.done += Math.max(now.done, earlier?.done ?? 0);
|
|
788
|
+
bytes.total += Math.max(now.total, earlier?.total ?? 0);
|
|
789
|
+
}
|
|
790
|
+
const step = installing === 0
|
|
791
|
+
? null
|
|
792
|
+
: downloading
|
|
793
|
+
? 'downloading'
|
|
794
|
+
: bytes.done < bytes.total
|
|
795
|
+
? 'writing'
|
|
796
|
+
: 'replaying';
|
|
797
|
+
return { step, rooms, bytes };
|
|
710
798
|
}
|
|
711
799
|
function readinessFor(port, reach) {
|
|
712
800
|
const required = requiredRooms(reach);
|
|
713
|
-
const
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
.filter((room) => room.state === state && !unindexable.has(room.room))
|
|
801
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
|
|
802
|
+
const preparing = coverage
|
|
803
|
+
.filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
|
|
717
804
|
.map(({ room }) => room);
|
|
718
|
-
const preparing = building('preparing');
|
|
719
805
|
if (preparing.length > 0)
|
|
720
806
|
prepare(preparing);
|
|
721
|
-
const indexing = building('indexing');
|
|
722
|
-
if (indexing.length > 0)
|
|
723
|
-
index(indexing, []);
|
|
724
807
|
return readinessOf({
|
|
725
808
|
code: port.code,
|
|
726
809
|
coverage: {
|
|
727
810
|
listed: required.listed,
|
|
728
811
|
rooms: coverage,
|
|
729
|
-
conversations: conversationCoverage(reach
|
|
730
|
-
search: search ? 'required' : 'unused',
|
|
812
|
+
conversations: conversationCoverage(reach),
|
|
731
813
|
},
|
|
732
814
|
persistence,
|
|
733
815
|
connection: connectionOf(),
|
|
734
816
|
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
? capacity
|
|
738
|
-
: port.capacity,
|
|
739
|
-
refused: refusedIn(required.rooms),
|
|
817
|
+
space: full ? 'full' : port.space,
|
|
818
|
+
installation: installationOf(coverage),
|
|
740
819
|
lost: storageLoss !== null,
|
|
741
|
-
established: targets[port.target]?.established === true
|
|
820
|
+
established: targets[port.target]?.established === true ||
|
|
821
|
+
(usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
|
|
742
822
|
});
|
|
743
823
|
}
|
|
744
824
|
function establish(name) {
|
|
745
825
|
targets = { ...targets, [name]: { established: true } };
|
|
746
826
|
queueReadiness();
|
|
827
|
+
probeEachMinute();
|
|
747
828
|
void durably((local) => local.fact('targets', targets)).catch(() => {
|
|
748
829
|
journal.record(`target record failed ${name}`);
|
|
749
830
|
});
|
|
@@ -781,87 +862,56 @@ export function composeRuntime(options) {
|
|
|
781
862
|
readinessWatches.delete(watch);
|
|
782
863
|
};
|
|
783
864
|
}
|
|
784
|
-
function
|
|
785
|
-
|
|
786
|
-
for (const { path, content } of page.entries)
|
|
787
|
-
bytes += (path.length + (content?.length ?? 0)) * 2;
|
|
788
|
-
return bytes;
|
|
789
|
-
}
|
|
790
|
-
function exceeded(error) {
|
|
791
|
-
return (typeof error === 'object' &&
|
|
792
|
-
error !== null &&
|
|
793
|
-
error.name === 'QuotaExceededError');
|
|
794
|
-
}
|
|
795
|
-
async function estimated() {
|
|
796
|
-
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
797
|
-
return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
|
|
798
|
-
}
|
|
799
|
-
function pause(locator, cause) {
|
|
800
|
-
paused.add(locator);
|
|
801
|
-
capacity = cause;
|
|
802
|
-
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
803
|
-
queueReadiness();
|
|
804
|
-
if (rechecking !== null)
|
|
865
|
+
function filled() {
|
|
866
|
+
if (full || closed)
|
|
805
867
|
return;
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
868
|
+
full = true;
|
|
869
|
+
journal.record('space full');
|
|
870
|
+
queueReadiness();
|
|
871
|
+
probeEachMinute();
|
|
810
872
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
return;
|
|
815
|
-
const available = await estimated().catch(() => null);
|
|
816
|
-
if (available === null || available < (held.needed ?? 0))
|
|
817
|
-
return;
|
|
818
|
-
if (held.cause === 'storage-exceeded' &&
|
|
819
|
-
(held.available === null || available <= held.available))
|
|
820
|
-
return;
|
|
821
|
-
resume();
|
|
873
|
+
function installedTarget() {
|
|
874
|
+
return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
|
|
875
|
+
Object.values(targets).some((target) => target.established));
|
|
822
876
|
}
|
|
823
|
-
function
|
|
824
|
-
if (
|
|
825
|
-
clearInterval(rechecking);
|
|
826
|
-
rechecking = null;
|
|
827
|
-
if (paused.size === 0 && capacity === null)
|
|
877
|
+
function probeEachMinute() {
|
|
878
|
+
if (!full || probing !== null || closed || !installedTarget())
|
|
828
879
|
return;
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
if (!closed && authorization !== undefined)
|
|
834
|
-
void sync('unstreamed');
|
|
835
|
-
}
|
|
836
|
-
async function reclaimed(local) {
|
|
837
|
-
const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
|
|
838
|
-
for (const locator of unreferenced) {
|
|
839
|
-
await local.abandon(locator);
|
|
840
|
-
transfers.delete(locator);
|
|
841
|
-
}
|
|
842
|
-
if (unreferenced.length > 0)
|
|
843
|
-
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
844
|
-
return unreferenced.length > 0;
|
|
880
|
+
// 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.
|
|
881
|
+
probing = setInterval(() => {
|
|
882
|
+
void probed();
|
|
883
|
+
}, 60_000);
|
|
845
884
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
if (available === null || available >= needed)
|
|
851
|
-
return true;
|
|
852
|
-
if (await reclaimed(local))
|
|
853
|
-
available = await estimated().catch(() => null);
|
|
854
|
-
if (available === null || available >= needed)
|
|
885
|
+
function probed() {
|
|
886
|
+
testing ??= database.ask({ kind: 'space.probe' }).then(() => {
|
|
887
|
+
testing = null;
|
|
888
|
+
spaceFreed();
|
|
855
889
|
return true;
|
|
856
|
-
|
|
857
|
-
|
|
890
|
+
}, () => {
|
|
891
|
+
testing = null;
|
|
892
|
+
return false;
|
|
893
|
+
});
|
|
894
|
+
return testing;
|
|
858
895
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
896
|
+
function spaceFreed() {
|
|
897
|
+
if (!full)
|
|
898
|
+
return;
|
|
899
|
+
full = false;
|
|
900
|
+
if (probing !== null)
|
|
901
|
+
clearInterval(probing);
|
|
902
|
+
probing = null;
|
|
903
|
+
unpreparable.clear();
|
|
904
|
+
journal.record('space freed');
|
|
905
|
+
queueReadiness();
|
|
906
|
+
if (closed || authorization === undefined)
|
|
907
|
+
return;
|
|
908
|
+
if (options.adapter.roomStream === undefined)
|
|
909
|
+
void sync('unstreamed', null);
|
|
910
|
+
else
|
|
911
|
+
void resynchronize();
|
|
862
912
|
}
|
|
863
913
|
function seedRoomHeld(room, version) {
|
|
864
|
-
return
|
|
914
|
+
return indexed(room) && reachedBy(room, version);
|
|
865
915
|
}
|
|
866
916
|
function seedHeld(at) {
|
|
867
917
|
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
@@ -898,7 +948,24 @@ export function composeRuntime(options) {
|
|
|
898
948
|
function sameCover(left, right) {
|
|
899
949
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
900
950
|
}
|
|
951
|
+
async function unheldRows(local, plan, rows, rooms) {
|
|
952
|
+
if (plan.shape.kind !== 'documents' || !bodied(plan.shape))
|
|
953
|
+
return [];
|
|
954
|
+
if (!('documents' in rows) || !textOnline())
|
|
955
|
+
return [];
|
|
956
|
+
const named = new Map();
|
|
957
|
+
for (const { room, path, textHeld } of rows.documents) {
|
|
958
|
+
if (textHeld === false)
|
|
959
|
+
named.set(room, [...(named.get(room) ?? []), path]);
|
|
960
|
+
}
|
|
961
|
+
const unheld = [];
|
|
962
|
+
for (const { locator, key } of rooms) {
|
|
963
|
+
unheld.push(...(await unheldIn(local, locator, key, named.get(locator) ?? [])));
|
|
964
|
+
}
|
|
965
|
+
return unheld.filter((one) => unholdable.get(textKey(one)) !== one.revision);
|
|
966
|
+
}
|
|
901
967
|
async function coveredRows(plan, cover) {
|
|
968
|
+
const asked = new Set();
|
|
902
969
|
for (;;) {
|
|
903
970
|
const before = cover();
|
|
904
971
|
if ('pending' in before)
|
|
@@ -909,8 +976,14 @@ export function composeRuntime(options) {
|
|
|
909
976
|
const current = () => local.changes(keys) === changes;
|
|
910
977
|
const rows = await windowRows(local, plan, before.rooms, current);
|
|
911
978
|
const after = cover();
|
|
912
|
-
if (rows
|
|
979
|
+
if (rows === null || !sameCover(before, after))
|
|
980
|
+
continue;
|
|
981
|
+
const unheld = (await unheldRows(local, plan, rows, before.rooms)).filter((one) => !asked.has(textKey(one)));
|
|
982
|
+
if (unheld.length === 0)
|
|
913
983
|
return { rows, cover: before, current };
|
|
984
|
+
for (const one of unheld)
|
|
985
|
+
asked.add(textKey(one));
|
|
986
|
+
await holdTexts(unheld);
|
|
914
987
|
}
|
|
915
988
|
}
|
|
916
989
|
async function windowRows(local, plan, rooms, unchanged) {
|
|
@@ -982,83 +1055,12 @@ export function composeRuntime(options) {
|
|
|
982
1055
|
const held = answered.cover;
|
|
983
1056
|
return { ...answered.rows, coverage: held.coverage };
|
|
984
1057
|
}
|
|
985
|
-
async function everyRow(plan) {
|
|
986
|
-
const window = { ...plan, limit: MAXIMUM_WINDOW, after: null, total: false };
|
|
987
|
-
const first = await rowsQuery(window);
|
|
988
|
-
if ('pending' in first || first.next === undefined)
|
|
989
|
-
return first;
|
|
990
|
-
const documents = 'documents' in first ? [...first.documents] : [];
|
|
991
|
-
const groups = 'groups' in first ? [...first.groups] : [];
|
|
992
|
-
for (let next = first.next; next !== undefined;) {
|
|
993
|
-
const values = JSON.parse(next);
|
|
994
|
-
const page = await rowsQuery({
|
|
995
|
-
...window,
|
|
996
|
-
after: { text: next, values: values.length === 0 ? null : values },
|
|
997
|
-
});
|
|
998
|
-
if ('pending' in page)
|
|
999
|
-
return page;
|
|
1000
|
-
if ('documents' in page)
|
|
1001
|
-
documents.push(...page.documents);
|
|
1002
|
-
else
|
|
1003
|
-
groups.push(...page.groups);
|
|
1004
|
-
if (JSON.stringify(page.coverage) !== JSON.stringify(first.coverage))
|
|
1005
|
-
return everyRow(plan);
|
|
1006
|
-
next = page.next;
|
|
1007
|
-
}
|
|
1008
|
-
return 'documents' in first
|
|
1009
|
-
? { documents, coverage: first.coverage }
|
|
1010
|
-
: { groups, coverage: first.coverage };
|
|
1011
|
-
}
|
|
1012
|
-
async function indexConversation(conversation) {
|
|
1013
|
-
searchIndex.indexMessages(conversation, [], false);
|
|
1014
|
-
let after = null;
|
|
1015
|
-
for (;;) {
|
|
1016
|
-
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
1017
|
-
if (closed)
|
|
1018
|
-
return;
|
|
1019
|
-
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
1020
|
-
if (batch.length === 0)
|
|
1021
|
-
return;
|
|
1022
|
-
after = batch[batch.length - 1].identity;
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
function indexNext() {
|
|
1026
|
-
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
1027
|
-
if (room !== undefined) {
|
|
1028
|
-
roomsToIndex.delete(room);
|
|
1029
|
-
return () => prepared.has(room)
|
|
1030
|
-
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
1031
|
-
: undefined;
|
|
1032
|
-
}
|
|
1033
|
-
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
1034
|
-
if (conversation === undefined)
|
|
1035
|
-
return null;
|
|
1036
|
-
conversationsToIndex.delete(conversation);
|
|
1037
|
-
return () => indexConversation(conversation);
|
|
1038
|
-
}
|
|
1039
|
-
function index(rooms, named) {
|
|
1040
|
-
for (const room of rooms)
|
|
1041
|
-
roomsToIndex.add(room);
|
|
1042
|
-
for (const conversation of named)
|
|
1043
|
-
conversationsToIndex.add(conversation);
|
|
1044
|
-
if (indexing)
|
|
1045
|
-
return;
|
|
1046
|
-
indexing = true;
|
|
1047
|
-
void (async () => {
|
|
1048
|
-
try {
|
|
1049
|
-
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
1050
|
-
await yielded();
|
|
1051
|
-
await unit();
|
|
1052
|
-
queueReadiness();
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
finally {
|
|
1056
|
-
indexing = false;
|
|
1057
|
-
}
|
|
1058
|
-
})().catch(() => { });
|
|
1059
|
-
}
|
|
1060
1058
|
function publishChangeSet(set) {
|
|
1061
1059
|
applyChangeSet(set);
|
|
1060
|
+
for (const { locator, path } of set.documents) {
|
|
1061
|
+
if (path !== null)
|
|
1062
|
+
attachedFiles.get(locator)?.delete(path);
|
|
1063
|
+
}
|
|
1062
1064
|
if (set.rooms.length > 0 ||
|
|
1063
1065
|
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
1064
1066
|
emitRooms();
|
|
@@ -1077,6 +1079,8 @@ export function composeRuntime(options) {
|
|
|
1077
1079
|
}
|
|
1078
1080
|
async function refuse(reason, provider = null) {
|
|
1079
1081
|
discoveryPaused = true;
|
|
1082
|
+
inFlight.abort();
|
|
1083
|
+
inFlight = new AbortController();
|
|
1080
1084
|
try {
|
|
1081
1085
|
if (reason === 'revoked-authorization') {
|
|
1082
1086
|
await exclusive(async () => {
|
|
@@ -1084,27 +1088,24 @@ export function composeRuntime(options) {
|
|
|
1084
1088
|
const records = [...replicas].map(([locator, held]) => ({
|
|
1085
1089
|
locator,
|
|
1086
1090
|
held,
|
|
1087
|
-
record: { ...held.record, model: revoked
|
|
1091
|
+
record: { ...held.record, model: revoked },
|
|
1088
1092
|
}));
|
|
1093
|
+
let answer = null;
|
|
1089
1094
|
try {
|
|
1090
1095
|
const local = await openLocal();
|
|
1091
|
-
await local.commit({
|
|
1096
|
+
answer = await local.commit({
|
|
1092
1097
|
rooms: records.map(({ locator, record }) => ({
|
|
1093
1098
|
locator,
|
|
1094
1099
|
key: record.key,
|
|
1095
1100
|
record: storedRecord(record),
|
|
1096
|
-
|
|
1101
|
+
composed: composedOf(composed(record)),
|
|
1097
1102
|
})),
|
|
1098
1103
|
});
|
|
1099
1104
|
}
|
|
1100
1105
|
catch {
|
|
1101
1106
|
journal.record('model revocation persistence failed');
|
|
1102
1107
|
}
|
|
1103
|
-
const keys = new Set(models.keys());
|
|
1104
|
-
for (const locator of rooms.keys()) {
|
|
1105
|
-
keys.add(modelKey(locator, 'collection'));
|
|
1106
|
-
keys.add(modelKey(locator, 'typed-selection'));
|
|
1107
|
-
}
|
|
1108
|
+
const keys = new Set([...models.keys(), ...rooms.keys()]);
|
|
1108
1109
|
for (const key of keys) {
|
|
1109
1110
|
invalidModels.add(key);
|
|
1110
1111
|
models.set(key, revoked);
|
|
@@ -1114,8 +1115,8 @@ export function composeRuntime(options) {
|
|
|
1114
1115
|
for (const { locator, held, record } of records) {
|
|
1115
1116
|
if (replicas.get(locator) !== held)
|
|
1116
1117
|
continue;
|
|
1117
|
-
|
|
1118
|
-
replicas.set(locator, { ...held, record });
|
|
1118
|
+
const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
|
|
1119
|
+
replicas.set(locator, { ...held, record, indexed });
|
|
1119
1120
|
}
|
|
1120
1121
|
for (const locator of [...rooms.keys()]) {
|
|
1121
1122
|
const room = rooms.get(locator);
|
|
@@ -1159,15 +1160,15 @@ export function composeRuntime(options) {
|
|
|
1159
1160
|
? null
|
|
1160
1161
|
: {
|
|
1161
1162
|
roots: selection.roots,
|
|
1162
|
-
model: modelTypes(
|
|
1163
|
+
model: modelTypes(builtIn),
|
|
1163
1164
|
},
|
|
1164
1165
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1165
1166
|
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
1166
1167
|
synchronizationAvailable,
|
|
1167
|
-
refusal: scopedRefusals.get(room.locator) ??
|
|
1168
|
+
refusal: scopedRefusals.get(room.locator) ?? null,
|
|
1168
1169
|
});
|
|
1169
1170
|
}
|
|
1170
|
-
async function applyDiscovery(found, started,
|
|
1171
|
+
async function applyDiscovery(found, started, left) {
|
|
1171
1172
|
const lockedBefore = lockedRooms();
|
|
1172
1173
|
for (const room of found) {
|
|
1173
1174
|
const fence = fences.get(room.locator);
|
|
@@ -1181,18 +1182,11 @@ export function composeRuntime(options) {
|
|
|
1181
1182
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
1182
1183
|
for (const locator of listedLocators)
|
|
1183
1184
|
scopedRefusals.delete(locator);
|
|
1184
|
-
if (
|
|
1185
|
+
if (left === undefined)
|
|
1185
1186
|
discovered.clear();
|
|
1186
|
-
else
|
|
1187
|
-
for (const locator of covered)
|
|
1188
|
-
discovered.delete(locator);
|
|
1189
1187
|
for (const room of listed)
|
|
1190
1188
|
discovered.set(room.locator, room);
|
|
1191
|
-
|
|
1192
|
-
if (options.adapter.exhaustiveDiscovery === true)
|
|
1193
|
-
await loseRooms(unlisted);
|
|
1194
|
-
else
|
|
1195
|
-
unlistedKept(unlisted);
|
|
1189
|
+
await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
|
|
1196
1190
|
for (const room of listed) {
|
|
1197
1191
|
const held = replicas.get(room.locator)?.record;
|
|
1198
1192
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -1223,15 +1217,6 @@ export function composeRuntime(options) {
|
|
|
1223
1217
|
]),
|
|
1224
1218
|
].filter((locator) => !listed.has(locator));
|
|
1225
1219
|
}
|
|
1226
|
-
function unlistedKept(unlisted) {
|
|
1227
|
-
for (const locator of unlisted) {
|
|
1228
|
-
const held = replicas.get(locator)?.record;
|
|
1229
|
-
if (held !== undefined)
|
|
1230
|
-
list(held, false);
|
|
1231
|
-
else
|
|
1232
|
-
rooms.delete(locator);
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
1220
|
function fence(locators) {
|
|
1236
1221
|
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1237
1222
|
const removed = fresh.map((locator) => ({
|
|
@@ -1242,36 +1227,33 @@ export function composeRuntime(options) {
|
|
|
1242
1227
|
fences.set(locator, { at: discoveries, erased: false });
|
|
1243
1228
|
lastFenced.set(locator, ++fencings);
|
|
1244
1229
|
confirmed.delete(locator);
|
|
1245
|
-
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1230
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []) {
|
|
1246
1231
|
pendings.delete(pendingKey(locator, batch.id));
|
|
1232
|
+
landedAhead.delete(pendingKey(locator, batch.id));
|
|
1233
|
+
}
|
|
1247
1234
|
replicas.delete(locator);
|
|
1235
|
+
highest.delete(locator);
|
|
1236
|
+
beyondHorizon.delete(locator);
|
|
1248
1237
|
queuesWithoutReplica.delete(locator);
|
|
1249
1238
|
rooms.delete(locator);
|
|
1250
1239
|
roomDrafts.delete(locator);
|
|
1251
|
-
|
|
1240
|
+
attachedFiles.delete(locator);
|
|
1241
|
+
untransferred(locator);
|
|
1242
|
+
startedOver.delete(locator);
|
|
1252
1243
|
catching.delete(locator);
|
|
1253
1244
|
discovered.delete(locator);
|
|
1254
|
-
announced.delete(locator);
|
|
1255
1245
|
unreadable.delete(locator);
|
|
1256
1246
|
stale.delete(locator);
|
|
1257
1247
|
repairable.delete(locator);
|
|
1258
|
-
|
|
1259
|
-
paused.delete(locator);
|
|
1260
|
-
prepared.delete(locator);
|
|
1248
|
+
unpreparable.delete(locator);
|
|
1261
1249
|
scopedRefusals.delete(locator);
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
const key = modelKey(locator, attached);
|
|
1266
|
-
models.delete(key);
|
|
1267
|
-
liveModels.delete(key);
|
|
1268
|
-
invalidModels.delete(key);
|
|
1269
|
-
}
|
|
1250
|
+
models.delete(locator);
|
|
1251
|
+
liveModels.delete(locator);
|
|
1252
|
+
invalidModels.delete(locator);
|
|
1270
1253
|
journal.record(`room lost ${locator}`);
|
|
1271
1254
|
}
|
|
1272
1255
|
if (fresh.length === 0)
|
|
1273
1256
|
return fresh;
|
|
1274
|
-
searchIndex.forget(fresh);
|
|
1275
1257
|
publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
|
|
1276
1258
|
liveQueries.recover();
|
|
1277
1259
|
moveTo({ ...status, ...attention() });
|
|
@@ -1315,7 +1297,14 @@ export function composeRuntime(options) {
|
|
|
1315
1297
|
});
|
|
1316
1298
|
await eraseFenced(fresh);
|
|
1317
1299
|
}
|
|
1318
|
-
function
|
|
1300
|
+
function retyped(persisted) {
|
|
1301
|
+
if (persisted.status === 'unavailable')
|
|
1302
|
+
return persisted;
|
|
1303
|
+
const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
|
|
1304
|
+
return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
|
|
1305
|
+
}
|
|
1306
|
+
function restored(key, held) {
|
|
1307
|
+
const persisted = retyped(held);
|
|
1319
1308
|
const prior = models.get(key);
|
|
1320
1309
|
if (prior !== undefined &&
|
|
1321
1310
|
prior.status !== 'unavailable' &&
|
|
@@ -1346,8 +1335,9 @@ export function composeRuntime(options) {
|
|
|
1346
1335
|
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1347
1336
|
targets = held.targets;
|
|
1348
1337
|
transfers.clear();
|
|
1349
|
-
|
|
1350
|
-
|
|
1338
|
+
arrivals.clear();
|
|
1339
|
+
streaming.clear();
|
|
1340
|
+
startedOver.clear();
|
|
1351
1341
|
limits = held.providerLimits;
|
|
1352
1342
|
capabilities = held.capabilities;
|
|
1353
1343
|
member = held.member;
|
|
@@ -1357,15 +1347,16 @@ export function composeRuntime(options) {
|
|
|
1357
1347
|
presentations.clear();
|
|
1358
1348
|
for (const record of held.presentation)
|
|
1359
1349
|
presentations.set(record.app, record.value);
|
|
1360
|
-
conversations.hydrate(heldConversations, chatDrafts);
|
|
1350
|
+
conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
|
|
1361
1351
|
pendings.clear();
|
|
1352
|
+
landedAhead.clear();
|
|
1362
1353
|
for (const queue of queues) {
|
|
1363
1354
|
for (const { batch, state } of queue.pending) {
|
|
1364
1355
|
pendings.set(pendingKey(queue.locator, batch.id), state);
|
|
1365
1356
|
}
|
|
1366
1357
|
}
|
|
1367
1358
|
replicas.clear();
|
|
1368
|
-
|
|
1359
|
+
reopened.clear();
|
|
1369
1360
|
queuesWithoutReplica.clear();
|
|
1370
1361
|
for (const [locator, queue] of queuedBatches) {
|
|
1371
1362
|
if (!held.rooms.has(locator))
|
|
@@ -1374,39 +1365,40 @@ export function composeRuntime(options) {
|
|
|
1374
1365
|
records.forEach((stored, index) => {
|
|
1375
1366
|
if (fences.has(stored.locator))
|
|
1376
1367
|
return;
|
|
1377
|
-
const
|
|
1378
|
-
|
|
1379
|
-
const record = { ...stored, model, selectionModel };
|
|
1368
|
+
const record = { ...stored, model: restored(stored.locator, stored.model) };
|
|
1369
|
+
reopened.add(record.locator);
|
|
1380
1370
|
replicas.set(record.locator, {
|
|
1381
1371
|
record,
|
|
1382
1372
|
queue: queuedBatches.get(record.locator) ?? [],
|
|
1383
1373
|
overlaid: held.overlaid.get(record.key) ?? new Set(),
|
|
1384
1374
|
count: counts[index] ?? 0,
|
|
1385
1375
|
confirmed: 0,
|
|
1376
|
+
indexed: !held.unindexed.has(record.key),
|
|
1386
1377
|
});
|
|
1387
|
-
if (!held.unprepared.has(record.key))
|
|
1388
|
-
prepared.add(record.locator);
|
|
1389
1378
|
const existing = rooms.get(record.locator);
|
|
1390
1379
|
if (existing === undefined)
|
|
1391
|
-
list(record,
|
|
1380
|
+
list(record, record.roomVersion !== null);
|
|
1392
1381
|
else {
|
|
1393
1382
|
rooms.set(record.locator, {
|
|
1394
1383
|
...existing,
|
|
1395
1384
|
roomVersion: record.roomVersion,
|
|
1396
1385
|
operations: record.operations,
|
|
1397
1386
|
replica: record.replica,
|
|
1398
|
-
model: modelTypes(model),
|
|
1387
|
+
model: modelTypes(record.model),
|
|
1399
1388
|
typedSelection: record.typedSelection === null
|
|
1400
1389
|
? null
|
|
1401
|
-
: {
|
|
1402
|
-
roots: record.typedSelection.roots,
|
|
1403
|
-
model: modelTypes(selectionModel),
|
|
1404
|
-
},
|
|
1390
|
+
: { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
|
|
1405
1391
|
});
|
|
1406
1392
|
}
|
|
1407
1393
|
});
|
|
1408
|
-
|
|
1409
|
-
|
|
1394
|
+
try {
|
|
1395
|
+
for (const key of held.orphans)
|
|
1396
|
+
await opened.release(key);
|
|
1397
|
+
}
|
|
1398
|
+
catch (error) {
|
|
1399
|
+
if (!refusedForSpace(error))
|
|
1400
|
+
throw error;
|
|
1401
|
+
}
|
|
1410
1402
|
if (held.erasing || held.orphans.length > 0)
|
|
1411
1403
|
queueMicrotask(sweep);
|
|
1412
1404
|
return {
|
|
@@ -1431,8 +1423,7 @@ export function composeRuntime(options) {
|
|
|
1431
1423
|
async function open() {
|
|
1432
1424
|
if (closed)
|
|
1433
1425
|
throw new Error('runtime closed');
|
|
1434
|
-
const opened =
|
|
1435
|
-
store = opened;
|
|
1426
|
+
const opened = openRuntimeStore(isFenced, database);
|
|
1436
1427
|
const interrupted = await opened.fences();
|
|
1437
1428
|
for (const locator of interrupted) {
|
|
1438
1429
|
fences.set(locator, { at: discoveries, erased: false });
|
|
@@ -1478,10 +1469,29 @@ export function composeRuntime(options) {
|
|
|
1478
1469
|
return opening;
|
|
1479
1470
|
}
|
|
1480
1471
|
function exclusive(work) {
|
|
1472
|
+
gathering = null;
|
|
1481
1473
|
const next = mutations.then(work, work);
|
|
1482
1474
|
mutations = next.then(() => undefined, () => undefined);
|
|
1483
1475
|
return next;
|
|
1484
1476
|
}
|
|
1477
|
+
function gathered(key, input, merge, work) {
|
|
1478
|
+
const tail = gathering;
|
|
1479
|
+
if (tail !== null && tail.key === key) {
|
|
1480
|
+
const merged = merge(tail.input, input);
|
|
1481
|
+
if (merged !== null) {
|
|
1482
|
+
tail.input = merged;
|
|
1483
|
+
return tail.done;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
const entry = { key, input, done: Promise.resolve() };
|
|
1487
|
+
entry.done = exclusive(() => {
|
|
1488
|
+
if (gathering === entry)
|
|
1489
|
+
gathering = null;
|
|
1490
|
+
return work(entry.input);
|
|
1491
|
+
});
|
|
1492
|
+
gathering = entry;
|
|
1493
|
+
return entry.done;
|
|
1494
|
+
}
|
|
1485
1495
|
function withdrawable(locator) {
|
|
1486
1496
|
return (batch) => {
|
|
1487
1497
|
const state = pendings.get(pendingKey(locator, batch.id));
|
|
@@ -1490,6 +1500,16 @@ export function composeRuntime(options) {
|
|
|
1490
1500
|
}
|
|
1491
1501
|
const REWRITE_LINKS = ['field', 'body'];
|
|
1492
1502
|
const CHECKED_LINKS = ['field', 'section'];
|
|
1503
|
+
const BODY_LINKS = ['body'];
|
|
1504
|
+
async function textsNeeded(local, record, named) {
|
|
1505
|
+
const paths = [...new Set(named.map((one) => (typeof one === 'string' ? one : one.path)))];
|
|
1506
|
+
const unheld = await unheldIn(local, record.locator, record.key, paths);
|
|
1507
|
+
if (unheld.length === 0)
|
|
1508
|
+
return null;
|
|
1509
|
+
await holdTexts(unheld);
|
|
1510
|
+
const [first] = await unheldIn(local, record.locator, record.key, unheld.map(({ path }) => path));
|
|
1511
|
+
return first?.path ?? null;
|
|
1512
|
+
}
|
|
1493
1513
|
async function sliceAt(record, view, queue, extra = [], moved = []) {
|
|
1494
1514
|
const local = await openLocal();
|
|
1495
1515
|
const needed = new Set([...touchedBy(queue), ...extra]);
|
|
@@ -1563,26 +1583,14 @@ export function composeRuntime(options) {
|
|
|
1563
1583
|
},
|
|
1564
1584
|
};
|
|
1565
1585
|
}
|
|
1566
|
-
function
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
fingerprint(compositionOf(before)) === fingerprint(compositionOf(after))) {
|
|
1570
|
-
return;
|
|
1571
|
-
}
|
|
1572
|
-
if (before !== undefined && before.key === after.key)
|
|
1573
|
-
prepared.delete(locator);
|
|
1574
|
-
}
|
|
1575
|
-
function committedRoom(locator, record, answer) {
|
|
1576
|
-
if (answer.prepared[locator] === false)
|
|
1577
|
-
prepared.delete(locator);
|
|
1578
|
-
else if (answer.prepared[locator] === true)
|
|
1579
|
-
prepared.add(locator);
|
|
1580
|
-
if (!prepared.has(locator))
|
|
1586
|
+
function committedRoom(locator, answer) {
|
|
1587
|
+
const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
|
|
1588
|
+
if (!indexed)
|
|
1581
1589
|
prepare([locator]);
|
|
1582
|
-
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
|
|
1590
|
+
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
|
|
1583
1591
|
}
|
|
1584
1592
|
function replayed(record, confirmedLayer, queue) {
|
|
1585
|
-
return stepped(replaying(confirmedLayer, queue, record.locator,
|
|
1593
|
+
return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
|
|
1586
1594
|
}
|
|
1587
1595
|
async function overlayDiff(record, before, visible, touched) {
|
|
1588
1596
|
const kept = before !== undefined && before.record.key === record.key
|
|
@@ -1602,8 +1610,6 @@ export function composeRuntime(options) {
|
|
|
1602
1610
|
function unpairedIn(locator, batch) {
|
|
1603
1611
|
if (!queueIn(locator).some((held) => held.id === batch.id))
|
|
1604
1612
|
return false;
|
|
1605
|
-
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
1606
|
-
return false;
|
|
1607
1613
|
return !leavingIn(batch, locator) || withdrawable(locator)(batch);
|
|
1608
1614
|
}
|
|
1609
1615
|
async function together(local, settlements) {
|
|
@@ -1645,11 +1651,17 @@ export function composeRuntime(options) {
|
|
|
1645
1651
|
const confirmedLayer = overrides === undefined
|
|
1646
1652
|
? read
|
|
1647
1653
|
: await stepped(slicingWith(read, overrides.set, overrides.removed));
|
|
1648
|
-
const composition =
|
|
1649
|
-
const
|
|
1650
|
-
|
|
1654
|
+
const composition = composed(record);
|
|
1655
|
+
const shown = queue.filter((batch) => {
|
|
1656
|
+
const held = landedAhead.get(pendingKey(locator, batch.id));
|
|
1657
|
+
return (held !== undefined &&
|
|
1658
|
+
Object.entries(held).some(([path, revision]) => confirmedLayer.revisions[path] !== revision));
|
|
1659
|
+
});
|
|
1660
|
+
const unshown = queue.filter((batch) => !shown.includes(batch));
|
|
1661
|
+
const { withdrawn, kept } = await stepped(standing(confirmedLayer, unshown, withdrawable(locator), localSaves, locator, composition));
|
|
1662
|
+
const founded = kept.filter((batch) => !unshown.includes(batch));
|
|
1651
1663
|
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
1652
|
-
const unqueued =
|
|
1664
|
+
const unqueued = shown.map((batch) => ({ batch, landed: true }));
|
|
1653
1665
|
for (const batch of withdrawn) {
|
|
1654
1666
|
unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
|
|
1655
1667
|
}
|
|
@@ -1688,6 +1700,7 @@ export function composeRuntime(options) {
|
|
|
1688
1700
|
key: record.key,
|
|
1689
1701
|
record: storedRecord(record),
|
|
1690
1702
|
roots: record.typedSelection?.roots ?? [],
|
|
1703
|
+
composed: composedOf(composition),
|
|
1691
1704
|
...settling.commit,
|
|
1692
1705
|
...(overrides === undefined
|
|
1693
1706
|
? {}
|
|
@@ -1713,8 +1726,10 @@ export function composeRuntime(options) {
|
|
|
1713
1726
|
commit,
|
|
1714
1727
|
apply: (answer) => {
|
|
1715
1728
|
for (const batch of previousQueue) {
|
|
1716
|
-
if (
|
|
1717
|
-
|
|
1729
|
+
if (staying.has(batch.id))
|
|
1730
|
+
continue;
|
|
1731
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1732
|
+
landedAhead.delete(pendingKey(locator, batch.id));
|
|
1718
1733
|
}
|
|
1719
1734
|
for (const draft of written.values())
|
|
1720
1735
|
rememberRoomDraft(draft);
|
|
@@ -1729,14 +1744,14 @@ export function composeRuntime(options) {
|
|
|
1729
1744
|
const confirmedChanged = settling.commit?.published !== undefined ||
|
|
1730
1745
|
settling.commit?.refreshed === true ||
|
|
1731
1746
|
overrides !== undefined;
|
|
1732
|
-
|
|
1733
|
-
const { count } = committedRoom(locator, record, answer);
|
|
1747
|
+
const { count, indexed } = committedRoom(locator, answer);
|
|
1734
1748
|
replicas.set(locator, {
|
|
1735
1749
|
record,
|
|
1736
1750
|
queue: kept,
|
|
1737
1751
|
overlaid: touched,
|
|
1738
1752
|
count,
|
|
1739
1753
|
confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
|
|
1754
|
+
indexed,
|
|
1740
1755
|
});
|
|
1741
1756
|
return mergeChangeSets([
|
|
1742
1757
|
changeSet(locator, {
|
|
@@ -1751,110 +1766,107 @@ export function composeRuntime(options) {
|
|
|
1751
1766
|
};
|
|
1752
1767
|
}
|
|
1753
1768
|
function adoptModels(locator, record) {
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
if (model.status === 'current')
|
|
1762
|
-
liveModels.set(key, JSON.stringify(model.snapshot));
|
|
1763
|
-
else if (model.status === 'unavailable')
|
|
1764
|
-
liveModels.delete(key);
|
|
1765
|
-
}
|
|
1769
|
+
const model = record.model;
|
|
1770
|
+
models.set(locator, model);
|
|
1771
|
+
invalidModels.delete(locator);
|
|
1772
|
+
if (model.status === 'current')
|
|
1773
|
+
liveModels.set(locator, JSON.stringify(model.snapshot));
|
|
1774
|
+
else if (model.status === 'unavailable')
|
|
1775
|
+
liveModels.delete(locator);
|
|
1766
1776
|
}
|
|
1767
1777
|
function remodel(locator, answers, held) {
|
|
1768
|
-
const
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
};
|
|
1779
|
-
return {
|
|
1780
|
-
collection: taken(answers?.collection, held.collection, 'collection'),
|
|
1781
|
-
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
1782
|
-
};
|
|
1778
|
+
const answer = answers?.collection;
|
|
1779
|
+
if (answer === undefined)
|
|
1780
|
+
return held;
|
|
1781
|
+
const { state, refused } = attachedModel(answer, held.collection, roomTypes);
|
|
1782
|
+
if (refused !== null) {
|
|
1783
|
+
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
1784
|
+
journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
|
|
1785
|
+
}
|
|
1786
|
+
models.set(locator, state);
|
|
1787
|
+
return { collection: state };
|
|
1783
1788
|
}
|
|
1784
|
-
function publish(local, room,
|
|
1785
|
-
|
|
1789
|
+
function publish(local, room, reached, landing, live) {
|
|
1790
|
+
const landed = exclusive(async () => {
|
|
1786
1791
|
if (!live()) {
|
|
1787
1792
|
caughtUpTo(room.locator);
|
|
1788
1793
|
return true;
|
|
1789
1794
|
}
|
|
1790
1795
|
const prior = replicas.get(room.locator);
|
|
1791
|
-
if (landing.
|
|
1792
|
-
(
|
|
1793
|
-
|
|
1796
|
+
if (!stands(room.locator, landing.from)) {
|
|
1797
|
+
if (landing.kind === 'refreshed') {
|
|
1798
|
+
await local.unrefresh(reached.key);
|
|
1799
|
+
return false;
|
|
1800
|
+
}
|
|
1801
|
+
caughtUpTo(room.locator);
|
|
1802
|
+
await abandoned(local, room.locator);
|
|
1794
1803
|
return false;
|
|
1795
1804
|
}
|
|
1805
|
+
const record = landing.kind === 'refreshed' && prior !== undefined
|
|
1806
|
+
? {
|
|
1807
|
+
...prior.record,
|
|
1808
|
+
roomVersion: reached.roomVersion,
|
|
1809
|
+
...(landing.remodelled ? { model: reached.model } : {}),
|
|
1810
|
+
}
|
|
1811
|
+
: reached;
|
|
1796
1812
|
adoptModels(room.locator, record);
|
|
1797
1813
|
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
1798
|
-
const roomVersion = record.roomVersion;
|
|
1799
|
-
const accepted = queue.filter((batch) => {
|
|
1800
|
-
const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
|
|
1801
|
-
return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
|
|
1802
|
-
});
|
|
1803
1814
|
let set;
|
|
1804
1815
|
try {
|
|
1805
|
-
set = await settle(local, room.locator, prior, record, queue
|
|
1816
|
+
set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
|
|
1806
1817
|
? {
|
|
1807
1818
|
room: true,
|
|
1808
1819
|
view: 'refreshed',
|
|
1809
|
-
commit: { refreshed: true
|
|
1820
|
+
commit: { refreshed: true },
|
|
1810
1821
|
}
|
|
1811
1822
|
: {
|
|
1812
1823
|
room: true,
|
|
1813
1824
|
view: landing.refreshed ? 'refreshed' : 'confirmed',
|
|
1814
1825
|
commit: {
|
|
1815
1826
|
published: { from: prior?.record.key ?? null },
|
|
1816
|
-
composition: compositionOf(record),
|
|
1817
1827
|
...(landing.refreshed ? { refreshed: true } : {}),
|
|
1818
1828
|
},
|
|
1819
1829
|
});
|
|
1820
1830
|
}
|
|
1821
1831
|
catch (error) {
|
|
1822
|
-
if (!
|
|
1832
|
+
if (!refusedForSpace(error))
|
|
1823
1833
|
throw error;
|
|
1824
|
-
await exceededOn(room.locator, null);
|
|
1825
1834
|
caughtUpTo(room.locator);
|
|
1826
1835
|
return true;
|
|
1827
1836
|
}
|
|
1828
1837
|
if (landing.kind === 'bootstrapped') {
|
|
1829
|
-
|
|
1838
|
+
highest.delete(room.locator);
|
|
1839
|
+
beyondHorizon.delete(room.locator);
|
|
1840
|
+
untransferred(room.locator);
|
|
1841
|
+
startedOver.delete(room.locator);
|
|
1830
1842
|
catching.delete(room.locator);
|
|
1831
|
-
|
|
1843
|
+
if (prior !== undefined && prior.record.key !== record.key)
|
|
1844
|
+
sweep();
|
|
1845
|
+
name(room.locator);
|
|
1832
1846
|
}
|
|
1833
|
-
for (const batch of accepted)
|
|
1834
|
-
pendings.delete(pendingKey(room.locator, batch.id));
|
|
1835
1847
|
stale.delete(room.locator);
|
|
1836
|
-
|
|
1848
|
+
unpreparable.delete(room.locator);
|
|
1837
1849
|
if (unreadable.delete(room.locator))
|
|
1838
1850
|
scopedRefusals.delete(room.locator);
|
|
1839
|
-
sourceRefusals.delete(room.locator);
|
|
1840
|
-
declarationRefusals.delete(room.locator);
|
|
1841
1851
|
list(room, true);
|
|
1842
1852
|
publishChangeSet(set);
|
|
1853
|
+
if (landing.kind === 'bootstrapped')
|
|
1854
|
+
void heldAgain(room.locator);
|
|
1843
1855
|
await unpair(local);
|
|
1844
1856
|
moveTo({ ...status, ...attention() });
|
|
1845
|
-
|
|
1846
|
-
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1857
|
+
if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1847
1858
|
submitLater(room.locator);
|
|
1848
1859
|
}
|
|
1849
1860
|
return true;
|
|
1850
1861
|
});
|
|
1862
|
+
return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
|
|
1851
1863
|
}
|
|
1852
1864
|
async function savedRoom(locator) {
|
|
1853
1865
|
const local = await openLocal();
|
|
1854
1866
|
const replica = replicas.get(locator);
|
|
1855
1867
|
const room = rooms.get(locator);
|
|
1856
1868
|
if (replica === undefined || room === undefined) {
|
|
1857
|
-
throw runtimeError('room
|
|
1869
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
1858
1870
|
}
|
|
1859
1871
|
return { local, replica, room };
|
|
1860
1872
|
}
|
|
@@ -1872,12 +1884,7 @@ export function composeRuntime(options) {
|
|
|
1872
1884
|
return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
|
|
1873
1885
|
}
|
|
1874
1886
|
async function saveParts(locator, named, writtenBy) {
|
|
1875
|
-
const read = named
|
|
1876
|
-
if (read.some(({ path }, at) => path === '' ||
|
|
1877
|
-
normalize(path) !== path ||
|
|
1878
|
-
read.findIndex((one) => one.path === path) !== at)) {
|
|
1879
|
-
throw runtimeError('room-draft-malformed');
|
|
1880
|
-
}
|
|
1887
|
+
const read = readSavedParts(named);
|
|
1881
1888
|
const target = await savedRoom(locator);
|
|
1882
1889
|
const saved = [];
|
|
1883
1890
|
const files = [];
|
|
@@ -1886,7 +1893,7 @@ export function composeRuntime(options) {
|
|
|
1886
1893
|
for (const { path, parts } of read) {
|
|
1887
1894
|
const file = parts.file === undefined
|
|
1888
1895
|
? undefined
|
|
1889
|
-
: await heldFile(parts.file.blob, binaryBase(visible, path));
|
|
1896
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
|
|
1890
1897
|
if (parts.file !== undefined && file !== undefined) {
|
|
1891
1898
|
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1892
1899
|
}
|
|
@@ -1921,13 +1928,27 @@ export function composeRuntime(options) {
|
|
|
1921
1928
|
const referring = removed.length === 0
|
|
1922
1929
|
? []
|
|
1923
1930
|
: (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
|
|
1924
|
-
const
|
|
1931
|
+
const unheld = await textsNeeded(local, record, [
|
|
1932
|
+
...paths,
|
|
1933
|
+
...referring,
|
|
1934
|
+
...(moved.length === 0
|
|
1935
|
+
? []
|
|
1936
|
+
: await local.referrers(record.key, 'confirmed', moved, REWRITE_LINKS)),
|
|
1937
|
+
...(removed.length === 0
|
|
1938
|
+
? []
|
|
1939
|
+
: await local.referrers(record.key, 'confirmed', removed, BODY_LINKS)),
|
|
1940
|
+
]);
|
|
1941
|
+
if (unheld !== null) {
|
|
1942
|
+
return { refusal: { code: 'transport-uncertainty', retryable: true, file: unheld } };
|
|
1943
|
+
}
|
|
1944
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
|
|
1925
1945
|
const { visible } = await replayed(record, confirmedLayer, replica.queue);
|
|
1926
|
-
const composition =
|
|
1946
|
+
const composition = composed(record);
|
|
1927
1947
|
return {
|
|
1928
1948
|
record,
|
|
1929
1949
|
confirmed: confirmedLayer,
|
|
1930
1950
|
visible,
|
|
1951
|
+
folders: new Set(await local.folders(record.key, 'visible', paths)),
|
|
1931
1952
|
*visibleOver(queue) {
|
|
1932
1953
|
return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
|
|
1933
1954
|
},
|
|
@@ -1939,13 +1960,7 @@ export function composeRuntime(options) {
|
|
|
1939
1960
|
return false;
|
|
1940
1961
|
const local = await openLocal();
|
|
1941
1962
|
const roots = record.typedSelection?.roots ?? [];
|
|
1942
|
-
|
|
1943
|
-
(await local.typedWithin(record.key, 'visible', roots, false))) {
|
|
1944
|
-
return true;
|
|
1945
|
-
}
|
|
1946
|
-
return (composition.selection !== null &&
|
|
1947
|
-
composition.selection.model.status === 'unavailable' &&
|
|
1948
|
-
(await local.typedWithin(record.key, 'visible', roots, true)));
|
|
1963
|
+
return local.typedWithin(record.key, 'visible', roots, false);
|
|
1949
1964
|
}
|
|
1950
1965
|
function textDiffers(left, right) {
|
|
1951
1966
|
return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
|
|
@@ -1956,18 +1971,6 @@ export function composeRuntime(options) {
|
|
|
1956
1971
|
return undefined;
|
|
1957
1972
|
return entry.binary !== null ? null : entry.content;
|
|
1958
1973
|
}
|
|
1959
|
-
function walkOf(local, key, view) {
|
|
1960
|
-
return async (after) => {
|
|
1961
|
-
const page = await local.walk(key, view, after, WALK_BYTES);
|
|
1962
|
-
return {
|
|
1963
|
-
entries: page.entries.map(({ path, content, binary }) => ({
|
|
1964
|
-
path,
|
|
1965
|
-
content: binary !== null ? null : content,
|
|
1966
|
-
})),
|
|
1967
|
-
next: page.next,
|
|
1968
|
-
};
|
|
1969
|
-
};
|
|
1970
|
-
}
|
|
1971
1974
|
function lookupOf(local, key, view) {
|
|
1972
1975
|
return async (paths) => {
|
|
1973
1976
|
if (paths.length === 0)
|
|
@@ -1984,37 +1987,6 @@ export function composeRuntime(options) {
|
|
|
1984
1987
|
const read = parseFrontmatter(entry.content ?? '');
|
|
1985
1988
|
return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
|
|
1986
1989
|
}
|
|
1987
|
-
function overriddenWalk(walk, overrides) {
|
|
1988
|
-
const sorted = [...overrides.keys()].sort();
|
|
1989
|
-
const beyond = (path) => {
|
|
1990
|
-
if (path === null)
|
|
1991
|
-
return 0;
|
|
1992
|
-
let low = 0;
|
|
1993
|
-
let high = sorted.length;
|
|
1994
|
-
while (low < high) {
|
|
1995
|
-
const middle = (low + high) >>> 1;
|
|
1996
|
-
if (sorted[middle] <= path)
|
|
1997
|
-
low = middle + 1;
|
|
1998
|
-
else
|
|
1999
|
-
high = middle;
|
|
2000
|
-
}
|
|
2001
|
-
return low;
|
|
2002
|
-
};
|
|
2003
|
-
return async (after) => {
|
|
2004
|
-
const page = await walk(after);
|
|
2005
|
-
const end = page.next;
|
|
2006
|
-
const added = sorted
|
|
2007
|
-
.slice(beyond(after), end === null ? sorted.length : beyond(end))
|
|
2008
|
-
.flatMap((path) => {
|
|
2009
|
-
const entry = overrides.get(path);
|
|
2010
|
-
return entry === null || entry === undefined
|
|
2011
|
-
? []
|
|
2012
|
-
: [{ path, content: entry.binary !== null ? null : entry.content }];
|
|
2013
|
-
});
|
|
2014
|
-
const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
2015
|
-
return { entries, next: end };
|
|
2016
|
-
};
|
|
2017
|
-
}
|
|
2018
1990
|
function overriddenLookup(lookup, overrides) {
|
|
2019
1991
|
return async (paths) => {
|
|
2020
1992
|
const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
|
|
@@ -2032,9 +2004,8 @@ export function composeRuntime(options) {
|
|
|
2032
2004
|
}
|
|
2033
2005
|
function validatorOf(record, confirmedLayer) {
|
|
2034
2006
|
return async (resulting, touched) => {
|
|
2035
|
-
const composition = compositionFor(record);
|
|
2036
2007
|
const local = await openLocal();
|
|
2037
|
-
const
|
|
2008
|
+
const changes = [];
|
|
2038
2009
|
const overrides = new Map();
|
|
2039
2010
|
const pace = paced();
|
|
2040
2011
|
for (const path of new Set([...confirmedLayer.held, ...touched])) {
|
|
@@ -2045,19 +2016,13 @@ export function composeRuntime(options) {
|
|
|
2045
2016
|
continue;
|
|
2046
2017
|
overrides.set(path, now);
|
|
2047
2018
|
if (textOf(was) !== textOf(now))
|
|
2048
|
-
|
|
2019
|
+
changes.push({ path, was: textOf(was), now: textOf(now) });
|
|
2049
2020
|
}
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
texts: textsOf(local, record.key, 'confirmed'),
|
|
2056
|
-
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2057
|
-
}));
|
|
2058
|
-
if (clean)
|
|
2059
|
-
return { verdict: 'accept' };
|
|
2060
|
-
return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
|
|
2021
|
+
return validatedNeighbourhood(changes, compositionFor(record), {
|
|
2022
|
+
lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
|
|
2023
|
+
texts: textsOf(local, record.key, 'confirmed'),
|
|
2024
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2025
|
+
});
|
|
2061
2026
|
};
|
|
2062
2027
|
}
|
|
2063
2028
|
function textsOf(local, key, view) {
|
|
@@ -2081,6 +2046,7 @@ export function composeRuntime(options) {
|
|
|
2081
2046
|
operations: room.operations,
|
|
2082
2047
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
2083
2048
|
visible: saving.visible,
|
|
2049
|
+
folders: saving.folders,
|
|
2084
2050
|
queued: queuedDocuments(replica.queue),
|
|
2085
2051
|
unmodeled: await unmodeledIn(record),
|
|
2086
2052
|
validate: validatorOf(record, saving.confirmed),
|
|
@@ -2101,6 +2067,7 @@ export function composeRuntime(options) {
|
|
|
2101
2067
|
locator,
|
|
2102
2068
|
key: saving.record.key,
|
|
2103
2069
|
roots: saving.record.typedSelection?.roots ?? [],
|
|
2070
|
+
composed: composedOf(composed(saving.record)),
|
|
2104
2071
|
overlay: diff.overlay,
|
|
2105
2072
|
});
|
|
2106
2073
|
}
|
|
@@ -2132,15 +2099,19 @@ export function composeRuntime(options) {
|
|
|
2132
2099
|
}
|
|
2133
2100
|
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
2134
2101
|
const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
|
|
2135
|
-
|
|
2102
|
+
if ('refusal' in sourceSaving)
|
|
2103
|
+
return { outcome: 'refused', ...sourceSaving.refusal };
|
|
2104
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
|
|
2136
2105
|
if (changed.length > 0)
|
|
2137
2106
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2138
2107
|
const target = replicas.get(destination);
|
|
2139
2108
|
const listedRoom = rooms.get(destination);
|
|
2140
2109
|
if (target === undefined || listedRoom === undefined) {
|
|
2141
|
-
throw runtimeError('room
|
|
2110
|
+
throw runtimeError('unknown-room', `Unknown Room ${destination}`);
|
|
2142
2111
|
}
|
|
2143
2112
|
const arriving = await savingIn(target, [], moves.map(({ to }) => to));
|
|
2113
|
+
if ('refusal' in arriving)
|
|
2114
|
+
return { outcome: 'refused', ...arriving.refusal };
|
|
2144
2115
|
const built = await buildMove({
|
|
2145
2116
|
source: await sideOf(locator, replica, room, sourceSaving),
|
|
2146
2117
|
destination: await sideOf(destination, target, listedRoom, arriving),
|
|
@@ -2201,10 +2172,10 @@ export function composeRuntime(options) {
|
|
|
2201
2172
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2202
2173
|
const { answer, touched } = committedBoth;
|
|
2203
2174
|
if (answer.fenced !== null) {
|
|
2204
|
-
throw runtimeError('room
|
|
2175
|
+
throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
|
|
2205
2176
|
}
|
|
2206
2177
|
if (fences.has(locator) || fences.has(destination)) {
|
|
2207
|
-
throw runtimeError('room
|
|
2178
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2208
2179
|
}
|
|
2209
2180
|
pendings.set(pendingKey(locator, id), {
|
|
2210
2181
|
drafts: saved,
|
|
@@ -2244,6 +2215,8 @@ export function composeRuntime(options) {
|
|
|
2244
2215
|
submitLater(locator);
|
|
2245
2216
|
}
|
|
2246
2217
|
async function committed(locator, target, saved, consumed, writtenBy, files = []) {
|
|
2218
|
+
if (full)
|
|
2219
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2247
2220
|
const into = movedInto(locator, saved);
|
|
2248
2221
|
if (into !== null) {
|
|
2249
2222
|
return committedMove(locator, into, target, saved, consumed, writtenBy, files);
|
|
@@ -2255,7 +2228,9 @@ export function composeRuntime(options) {
|
|
|
2255
2228
|
...saved.map(({ path }) => path),
|
|
2256
2229
|
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
2257
2230
|
]);
|
|
2258
|
-
|
|
2231
|
+
if ('refusal' in saving)
|
|
2232
|
+
return { outcome: 'refused', ...saving.refusal };
|
|
2233
|
+
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
|
|
2259
2234
|
if (changed.length > 0)
|
|
2260
2235
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2261
2236
|
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
|
|
@@ -2283,6 +2258,7 @@ export function composeRuntime(options) {
|
|
|
2283
2258
|
providerLimits: limits,
|
|
2284
2259
|
handle: room.handle,
|
|
2285
2260
|
visible: saving.visible,
|
|
2261
|
+
folders: saving.folders,
|
|
2286
2262
|
changes,
|
|
2287
2263
|
bases: parts.flatMap(({ bases }) => bases),
|
|
2288
2264
|
queued: queuedDocuments(replica.queue),
|
|
@@ -2372,10 +2348,10 @@ export function composeRuntime(options) {
|
|
|
2372
2348
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2373
2349
|
const { answer, touched } = done;
|
|
2374
2350
|
if (answer.fenced !== null) {
|
|
2375
|
-
throw runtimeError('room
|
|
2351
|
+
throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
|
|
2376
2352
|
}
|
|
2377
2353
|
if (fences.has(locator))
|
|
2378
|
-
throw runtimeError('room
|
|
2354
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2379
2355
|
for (const origin of withdrawn.keys())
|
|
2380
2356
|
pendings.delete(pendingKey(locator, origin));
|
|
2381
2357
|
for (const [id, drafts] of drafted) {
|
|
@@ -2424,7 +2400,7 @@ export function composeRuntime(options) {
|
|
|
2424
2400
|
throw new Error('connection refused');
|
|
2425
2401
|
await local.keepUpload(locator, identifier, upload);
|
|
2426
2402
|
if (fences.has(locator))
|
|
2427
|
-
throw runtimeError('room
|
|
2403
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2428
2404
|
const key = pendingKey(locator, identifier);
|
|
2429
2405
|
pendings.set(key, { ...pendings.get(key), upload });
|
|
2430
2406
|
if (!continuing())
|
|
@@ -2433,6 +2409,9 @@ export function composeRuntime(options) {
|
|
|
2433
2409
|
async function readHistory(locator, path, query = {}) {
|
|
2434
2410
|
if (closed || refusal !== null)
|
|
2435
2411
|
return UNCERTAIN;
|
|
2412
|
+
await firstDiscovery;
|
|
2413
|
+
if (!roomsNamed)
|
|
2414
|
+
return UNCERTAIN;
|
|
2436
2415
|
try {
|
|
2437
2416
|
await ensureAuthorization();
|
|
2438
2417
|
}
|
|
@@ -2522,14 +2501,12 @@ export function composeRuntime(options) {
|
|
|
2522
2501
|
return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
|
|
2523
2502
|
};
|
|
2524
2503
|
}
|
|
2525
|
-
function binaryRoom(request) {
|
|
2526
|
-
if (request.kind === 'path')
|
|
2527
|
-
return request.locator;
|
|
2528
|
-
return options.adapter.referrerScope(request.referrer)?.locator ?? null;
|
|
2529
|
-
}
|
|
2530
2504
|
async function transferBinary(request, transfer, answer) {
|
|
2531
2505
|
if (closed || refusal !== null)
|
|
2532
2506
|
return UNCERTAIN;
|
|
2507
|
+
await firstDiscovery;
|
|
2508
|
+
if (!roomsNamed)
|
|
2509
|
+
return UNCERTAIN;
|
|
2533
2510
|
try {
|
|
2534
2511
|
await ensureAuthorization();
|
|
2535
2512
|
}
|
|
@@ -2539,16 +2516,13 @@ export function composeRuntime(options) {
|
|
|
2539
2516
|
if (closed || authorization === undefined || refusal !== null)
|
|
2540
2517
|
return UNCERTAIN;
|
|
2541
2518
|
const since = fencings;
|
|
2542
|
-
journal.record(request.
|
|
2543
|
-
? `transfer binary ${request.locator} ${request.path}`
|
|
2544
|
-
: `transfer binary ${request.fileRef}`);
|
|
2519
|
+
journal.record(`transfer binary ${request.locator} ${request.path}`);
|
|
2545
2520
|
try {
|
|
2546
2521
|
const result = await transfer(authorization);
|
|
2547
2522
|
const settled = result;
|
|
2548
2523
|
journal.record(`outcome binary ${settled.kind}`);
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
return failure({ kind: 'room-not-readable', locator });
|
|
2524
|
+
if (fencedSince(request.locator, since))
|
|
2525
|
+
return failure({ kind: 'room-not-readable', locator: request.locator });
|
|
2552
2526
|
if (settled.kind === 'scoped-refusal') {
|
|
2553
2527
|
const room = replicas.get(settled.locator)?.record;
|
|
2554
2528
|
if (room !== undefined)
|
|
@@ -2563,23 +2537,104 @@ export function composeRuntime(options) {
|
|
|
2563
2537
|
return UNCERTAIN;
|
|
2564
2538
|
}
|
|
2565
2539
|
}
|
|
2540
|
+
async function heldAgain(locator) {
|
|
2541
|
+
const replica = replicas.get(locator);
|
|
2542
|
+
if (replica === undefined || !textOnline())
|
|
2543
|
+
return;
|
|
2544
|
+
try {
|
|
2545
|
+
const wanted = await (await openLocal()).wanted(locator, replica.record.key);
|
|
2546
|
+
await holdTexts(wanted.map(({ path, revision }) => ({ locator, path, revision })));
|
|
2547
|
+
}
|
|
2548
|
+
catch {
|
|
2549
|
+
journal.record(`hold text ${locator} unwritten`);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
function textOnline() {
|
|
2553
|
+
return !closed && refusal === null && connectionOf() === 'connected';
|
|
2554
|
+
}
|
|
2555
|
+
function textKey({ locator, path }) {
|
|
2556
|
+
return documentKey(locator, path);
|
|
2557
|
+
}
|
|
2558
|
+
async function readText({ locator, path, revision }) {
|
|
2559
|
+
const replica = replicas.get(locator);
|
|
2560
|
+
if (replica === undefined || !textOnline())
|
|
2561
|
+
return;
|
|
2562
|
+
try {
|
|
2563
|
+
await ensureAuthorization();
|
|
2564
|
+
}
|
|
2565
|
+
catch {
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
2569
|
+
return;
|
|
2570
|
+
const since = fencings;
|
|
2571
|
+
journal.record(`hold text ${locator} ${path}`);
|
|
2572
|
+
const read = await reads
|
|
2573
|
+
.bootstrap(authorization, { locator, selection: replica.record.replica.selection, path })
|
|
2574
|
+
.catch(() => null);
|
|
2575
|
+
journal.record(`outcome text ${read?.kind ?? 'transport-uncertainty'}`);
|
|
2576
|
+
if (read === null || read.kind !== 'bootstrap' || fencedSince(locator, since))
|
|
2577
|
+
return;
|
|
2578
|
+
const content = read.documents[path];
|
|
2579
|
+
const now = replicas.get(locator);
|
|
2580
|
+
if (content === undefined || read.revisions[path] !== revision || now === undefined) {
|
|
2581
|
+
unholdable.set(textKey({ locator, path, revision }), revision);
|
|
2582
|
+
return;
|
|
2583
|
+
}
|
|
2584
|
+
const held = await (await openLocal()).hold(locator, now.record.key, compositionFor(now.record), [{ path, revision, content }]);
|
|
2585
|
+
if (held.length === 0)
|
|
2586
|
+
unholdable.set(textKey({ locator, path, revision }), revision);
|
|
2587
|
+
else
|
|
2588
|
+
publishChangeSet(changeSet(locator, { documents: held }));
|
|
2589
|
+
}
|
|
2590
|
+
function holdText(unheld) {
|
|
2591
|
+
const key = textKey(unheld);
|
|
2592
|
+
const running = holding.get(key);
|
|
2593
|
+
if (running !== undefined)
|
|
2594
|
+
return running;
|
|
2595
|
+
const started = readText(unheld).finally(() => holding.delete(key));
|
|
2596
|
+
holding.set(key, started);
|
|
2597
|
+
return started;
|
|
2598
|
+
}
|
|
2599
|
+
async function holdTexts(wanted) {
|
|
2600
|
+
const queue = wanted.filter((one) => unholdable.get(textKey(one)) !== one.revision);
|
|
2601
|
+
await Promise.all(Array.from({ length: Math.min(TEXT_READS, queue.length) }, async () => {
|
|
2602
|
+
for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
|
|
2603
|
+
await holdText(next);
|
|
2604
|
+
}
|
|
2605
|
+
}));
|
|
2606
|
+
}
|
|
2607
|
+
async function unheldIn(local, locator, key, paths) {
|
|
2608
|
+
if (paths.length === 0)
|
|
2609
|
+
return [];
|
|
2610
|
+
return (await local.entries(key, 'confirmed', paths)).flatMap((entry) => entry.content === null &&
|
|
2611
|
+
entry.binary === null &&
|
|
2612
|
+
entry.outline !== null &&
|
|
2613
|
+
entry.revision !== null
|
|
2614
|
+
? [{ locator, path: entry.path, revision: entry.revision }]
|
|
2615
|
+
: []);
|
|
2616
|
+
}
|
|
2617
|
+
function fetchBinary(request, name) {
|
|
2618
|
+
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
2619
|
+
}
|
|
2566
2620
|
async function readBinary(request, name) {
|
|
2567
2621
|
if (closed)
|
|
2568
2622
|
return UNCERTAIN;
|
|
2569
2623
|
const held = await keptFiles.find(name);
|
|
2570
2624
|
if (held !== null)
|
|
2571
2625
|
return held;
|
|
2572
|
-
return
|
|
2626
|
+
return fetchBinary(request, name);
|
|
2573
2627
|
}
|
|
2574
2628
|
async function binaryPath(locator, path, revision) {
|
|
2575
2629
|
if (revision !== undefined)
|
|
2576
2630
|
return { kind: 'path', locator, path, revision };
|
|
2577
2631
|
const replica = replicas.get(locator);
|
|
2578
|
-
if (replica === undefined)
|
|
2579
|
-
return null;
|
|
2580
2632
|
const local = await openLocal();
|
|
2581
|
-
const [held] = await local.entries(replica.record.key, 'visible', [path]);
|
|
2582
|
-
|
|
2633
|
+
const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
|
|
2634
|
+
const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
|
|
2635
|
+
if (attached !== undefined)
|
|
2636
|
+
return { kind: 'path', locator, path, revision: attached.revision };
|
|
2637
|
+
if (replica === undefined || held?.binary === undefined || held.binary === null)
|
|
2583
2638
|
return null;
|
|
2584
2639
|
if (held.binary.state === 'pending')
|
|
2585
2640
|
return { kind: 'pending', localId: held.binary.localId };
|
|
@@ -2595,7 +2650,7 @@ export function composeRuntime(options) {
|
|
|
2595
2650
|
const holds = (at) => {
|
|
2596
2651
|
const kept = confirmedLayer.binaries[at];
|
|
2597
2652
|
return (kept?.state === 'accepted' &&
|
|
2598
|
-
kept.
|
|
2653
|
+
kept.sha256 === record.sha256 &&
|
|
2599
2654
|
confirmedLayer.revisions[at] === record.revision);
|
|
2600
2655
|
};
|
|
2601
2656
|
let confirmedAt = path;
|
|
@@ -2609,57 +2664,101 @@ export function composeRuntime(options) {
|
|
|
2609
2664
|
locator,
|
|
2610
2665
|
path: confirmedAt,
|
|
2611
2666
|
revision: record.revision,
|
|
2612
|
-
fileRef: record.fileRef,
|
|
2613
2667
|
};
|
|
2614
2668
|
}
|
|
2615
|
-
async function
|
|
2669
|
+
async function askedBinary(locator, path, revision) {
|
|
2616
2670
|
await ready;
|
|
2617
2671
|
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2618
|
-
|
|
2672
|
+
return drafted !== undefined && drafted.localId === revision
|
|
2619
2673
|
? { kind: 'pending', localId: revision }
|
|
2620
|
-
:
|
|
2674
|
+
: binaryPath(locator, path, revision);
|
|
2675
|
+
}
|
|
2676
|
+
function pendingBinary(locator, path, localId) {
|
|
2677
|
+
const facts = queuedFacts(locator, path, localId);
|
|
2678
|
+
return facts === null
|
|
2679
|
+
? absentBinary(path)
|
|
2680
|
+
: {
|
|
2681
|
+
kind: 'binary',
|
|
2682
|
+
file: localId,
|
|
2683
|
+
byteLength: facts.byteLength,
|
|
2684
|
+
mediaType: facts.mediaType,
|
|
2685
|
+
};
|
|
2686
|
+
}
|
|
2687
|
+
async function readBinaryPath(locator, path, revision) {
|
|
2688
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2621
2689
|
if (asked === null)
|
|
2622
2690
|
return absentBinary(path);
|
|
2623
|
-
if (asked.kind === 'pending')
|
|
2624
|
-
|
|
2625
|
-
return facts === null
|
|
2626
|
-
? absentBinary(path)
|
|
2627
|
-
: {
|
|
2628
|
-
kind: 'binary',
|
|
2629
|
-
file: asked.localId,
|
|
2630
|
-
byteLength: facts.byteLength,
|
|
2631
|
-
mediaType: facts.mediaType,
|
|
2632
|
-
};
|
|
2633
|
-
}
|
|
2691
|
+
if (asked.kind === 'pending')
|
|
2692
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2634
2693
|
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
2635
2694
|
}
|
|
2695
|
+
async function heldMediaType(locator, path) {
|
|
2696
|
+
const replica = replicas.get(locator);
|
|
2697
|
+
const [held] = replica === undefined
|
|
2698
|
+
? []
|
|
2699
|
+
: await (await openLocal()).entries(replica.record.key, 'visible', [path]);
|
|
2700
|
+
return held === undefined
|
|
2701
|
+
? attachedFiles.get(locator)?.get(path)?.mediaType
|
|
2702
|
+
: held.binary?.mediaType;
|
|
2703
|
+
}
|
|
2704
|
+
function linkOf(asked, held) {
|
|
2705
|
+
return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
|
|
2706
|
+
}
|
|
2636
2707
|
async function linkBinaryPath(locator, path, revision) {
|
|
2637
2708
|
await ready;
|
|
2638
2709
|
const asked = await binaryPath(locator, path, revision);
|
|
2639
2710
|
if (asked === null || asked.kind === 'pending')
|
|
2640
2711
|
return absentBinary(path);
|
|
2641
|
-
return
|
|
2712
|
+
return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
|
|
2642
2713
|
}
|
|
2643
|
-
async function
|
|
2714
|
+
async function showBinaryPath(locator, path, revision) {
|
|
2715
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2716
|
+
if (asked === null)
|
|
2717
|
+
return absentBinary(path);
|
|
2718
|
+
if (asked.kind === 'pending')
|
|
2719
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2720
|
+
if (closed)
|
|
2721
|
+
return UNCERTAIN;
|
|
2722
|
+
const name = { locator, path: asked.path, revision: asked.revision };
|
|
2723
|
+
const kept = await keptFiles.find(name);
|
|
2724
|
+
if (kept !== null)
|
|
2725
|
+
return kept;
|
|
2726
|
+
const mediaType = await heldMediaType(locator, path);
|
|
2727
|
+
if (mediaType?.startsWith('image/') === true)
|
|
2728
|
+
return fetchBinary(asked, name);
|
|
2729
|
+
return linkOf(asked, revision === undefined ? mediaType : undefined);
|
|
2730
|
+
}
|
|
2731
|
+
async function attachBinary(locator, file) {
|
|
2644
2732
|
await ready;
|
|
2645
|
-
|
|
2646
|
-
if (
|
|
2647
|
-
return
|
|
2648
|
-
|
|
2733
|
+
await firstDiscovery;
|
|
2734
|
+
if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
|
|
2735
|
+
return OFFLINE;
|
|
2736
|
+
const request = await attachedFile(locator, file);
|
|
2737
|
+
return transferBinary({ locator, path: file.name }, async (held) => {
|
|
2738
|
+
const result = await options.adapter.attach(held, request);
|
|
2739
|
+
if (result.kind === 'attached') {
|
|
2740
|
+
const landed = attachedFiles.get(locator) ?? new Map();
|
|
2741
|
+
landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
|
|
2742
|
+
attachedFiles.set(locator, landed);
|
|
2743
|
+
}
|
|
2744
|
+
return result;
|
|
2745
|
+
}, attachAnswer);
|
|
2649
2746
|
}
|
|
2650
2747
|
function readPart(file, part) {
|
|
2651
|
-
return
|
|
2748
|
+
return database.ask({ kind: 'file.part', key: file, part });
|
|
2652
2749
|
}
|
|
2653
2750
|
async function cleanUp() {
|
|
2654
|
-
const freed = await
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2751
|
+
const freed = await database.ask({ kind: 'opened.clean' });
|
|
2752
|
+
const local = await openLocal();
|
|
2753
|
+
let texts = 0;
|
|
2754
|
+
for (const [locator, replica] of replicas) {
|
|
2755
|
+
const released = await local.unhold(locator, replica.record.key, compositionFor(replica.record));
|
|
2756
|
+
texts += released.bytes;
|
|
2757
|
+
if (released.paths.length > 0) {
|
|
2758
|
+
publishChangeSet(changeSet(locator, { documents: released.paths }));
|
|
2759
|
+
}
|
|
2661
2760
|
}
|
|
2662
|
-
return
|
|
2761
|
+
return freed + texts + ((await options.shell.cleanUp?.()) ?? 0);
|
|
2663
2762
|
}
|
|
2664
2763
|
function declaredAction(name) {
|
|
2665
2764
|
for (const capability of capabilities) {
|
|
@@ -2712,6 +2811,24 @@ export function composeRuntime(options) {
|
|
|
2712
2811
|
return UNCERTAIN;
|
|
2713
2812
|
}
|
|
2714
2813
|
}
|
|
2814
|
+
async function readConversationHistory(request) {
|
|
2815
|
+
await ready;
|
|
2816
|
+
const stream = options.adapter.conversations;
|
|
2817
|
+
if (closed || refusal !== null || stream?.history === undefined)
|
|
2818
|
+
return UNCERTAIN;
|
|
2819
|
+
journal.record('transfer conversation history');
|
|
2820
|
+
try {
|
|
2821
|
+
const result = await stream.history(request);
|
|
2822
|
+
if (result.kind === 'revoked-authorization')
|
|
2823
|
+
await refuse('revoked-authorization');
|
|
2824
|
+
journal.record(`outcome conversation history ${result.kind}`);
|
|
2825
|
+
return calledResult(result);
|
|
2826
|
+
}
|
|
2827
|
+
catch {
|
|
2828
|
+
journal.record('outcome conversation history transport-uncertainty');
|
|
2829
|
+
return UNCERTAIN;
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2715
2832
|
function unsettled(result) {
|
|
2716
2833
|
return result.kind !== 'action' && result.retryable;
|
|
2717
2834
|
}
|
|
@@ -2773,6 +2890,33 @@ export function composeRuntime(options) {
|
|
|
2773
2890
|
const held = replicas.get(locator);
|
|
2774
2891
|
return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
|
|
2775
2892
|
}
|
|
2893
|
+
function shownAt(locator) {
|
|
2894
|
+
const held = replicas.get(locator)?.record.roomVersion;
|
|
2895
|
+
const raised = highest.get(locator);
|
|
2896
|
+
if (held === undefined || raised === undefined)
|
|
2897
|
+
return held ?? raised;
|
|
2898
|
+
return options.adapter.reached(locator, held, raised) ? held : raised;
|
|
2899
|
+
}
|
|
2900
|
+
function showing(locator, roomVersion) {
|
|
2901
|
+
const at = shownAt(locator);
|
|
2902
|
+
if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
|
|
2903
|
+
highest.set(locator, roomVersion);
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
function older(locator, roomVersion) {
|
|
2907
|
+
if (beyondHorizon.has(locator))
|
|
2908
|
+
return false;
|
|
2909
|
+
const at = shownAt(locator);
|
|
2910
|
+
return (at !== undefined &&
|
|
2911
|
+
options.adapter.reached(locator, at, roomVersion) &&
|
|
2912
|
+
!options.adapter.reached(locator, roomVersion, at));
|
|
2913
|
+
}
|
|
2914
|
+
function replaced(locators) {
|
|
2915
|
+
for (const locator of locators) {
|
|
2916
|
+
highest.delete(locator);
|
|
2917
|
+
beyondHorizon.add(locator);
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2776
2920
|
async function writtenAfter(locator, roomVersion, path) {
|
|
2777
2921
|
const held = replicas.get(locator);
|
|
2778
2922
|
if (held === undefined)
|
|
@@ -2783,169 +2927,233 @@ export function composeRuntime(options) {
|
|
|
2783
2927
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
2784
2928
|
!options.adapter.reached(locator, roomVersion, written));
|
|
2785
2929
|
}
|
|
2786
|
-
function
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
else
|
|
2796
|
-
known.set(path, entry.binary !== null ? null : entry.content);
|
|
2797
|
-
}
|
|
2798
|
-
return known;
|
|
2799
|
-
};
|
|
2800
|
-
}
|
|
2801
|
-
async function unreadableRoom(texts, walk, paths) {
|
|
2802
|
-
if (paths !== null) {
|
|
2803
|
-
const text = {};
|
|
2804
|
-
for (const [path, content] of await texts(paths))
|
|
2805
|
-
if (content !== null)
|
|
2806
|
-
text[path] = content;
|
|
2807
|
-
return stepped(unreadableIn(text, Object.keys(text)));
|
|
2808
|
-
}
|
|
2809
|
-
for (let after = null;;) {
|
|
2810
|
-
const page = await walk(after);
|
|
2811
|
-
const text = {};
|
|
2812
|
-
for (const { path, content } of page.entries)
|
|
2813
|
-
if (content !== null)
|
|
2814
|
-
text[path] = content;
|
|
2815
|
-
const refused = await stepped(unreadableIn(text, Object.keys(text)));
|
|
2816
|
-
if (refused !== null)
|
|
2817
|
-
return refused;
|
|
2818
|
-
if (page.next === null)
|
|
2819
|
-
return null;
|
|
2820
|
-
after = page.next;
|
|
2821
|
-
}
|
|
2822
|
-
}
|
|
2823
|
-
async function validatedRoom(local, record, view, previous, overrides = null) {
|
|
2824
|
-
const composition = compositionOf(record);
|
|
2825
|
-
const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
|
|
2826
|
-
const lookup = overrides === null
|
|
2827
|
-
? lookupOf(local, record.key, view)
|
|
2828
|
-
: overriddenLookup(lookupOf(local, record.key, view), overrides);
|
|
2829
|
-
const texts = overrides === null
|
|
2830
|
-
? textsOf(local, record.key, view)
|
|
2831
|
-
: overriddenTexts(textsOf(local, record.key, view), overrides);
|
|
2832
|
-
const walk = overrides === null
|
|
2833
|
-
? walkOf(local, record.key, view)
|
|
2834
|
-
: overriddenWalk(walkOf(local, record.key, view), overrides);
|
|
2835
|
-
let changes = null;
|
|
2836
|
-
let reshaped = false;
|
|
2837
|
-
if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
|
|
2838
|
-
const [was, now] = await Promise.all([
|
|
2839
|
-
local.entries(record.key, 'confirmed', previous.changed),
|
|
2840
|
-
overrides === null
|
|
2841
|
-
? local.entries(record.key, view, previous.changed)
|
|
2842
|
-
: Promise.resolve([]),
|
|
2843
|
-
]);
|
|
2844
|
-
const before = new Map(was.map((entry) => [entry.path, entry]));
|
|
2845
|
-
const after = overrides === null
|
|
2846
|
-
? new Map(now.map((entry) => [entry.path, entry]))
|
|
2847
|
-
: overrides;
|
|
2848
|
-
changes = previous.changed.flatMap((path) => {
|
|
2849
|
-
const one = before.get(path) ?? null;
|
|
2850
|
-
const other = after.get(path) ?? null;
|
|
2851
|
-
reshaped ||= textDiffers(one, other);
|
|
2852
|
-
const held = textOf(one);
|
|
2853
|
-
const next = textOf(other);
|
|
2854
|
-
return held === next ? [] : [{ path, was: held, now: next }];
|
|
2855
|
-
});
|
|
2856
|
-
}
|
|
2857
|
-
const named = changes === null ? null : changes.map(({ path }) => path);
|
|
2858
|
-
const textChanged = changes === null || changes.length > 0 || reshaped;
|
|
2859
|
-
const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
|
|
2860
|
-
if (modeled && (textChanged || modelsChanged)) {
|
|
2861
|
-
const clean = previous !== null &&
|
|
2862
|
-
changes !== null &&
|
|
2863
|
-
(await changedCleanly(changes, compositionOf(previous.record), composition, {
|
|
2864
|
-
lookup,
|
|
2865
|
-
texts,
|
|
2866
|
-
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2867
|
-
}));
|
|
2868
|
-
if (clean)
|
|
2869
|
-
return null;
|
|
2870
|
-
const verdict = await validatedCollection(walk, lookup, composition);
|
|
2871
|
-
if (verdict.verdict === 'accept')
|
|
2872
|
-
return null;
|
|
2873
|
-
if (verdict.rule !== 'model-unavailable')
|
|
2874
|
-
return { kind: 'refused', refusal: verdict };
|
|
2875
|
-
return unreadableRoom(texts, walk, named);
|
|
2876
|
-
}
|
|
2877
|
-
if (textChanged)
|
|
2878
|
-
return unreadableRoom(texts, walk, named);
|
|
2879
|
-
return null;
|
|
2930
|
+
async function writtenLater(local, locator, key, paths, roomVersion) {
|
|
2931
|
+
if (paths.length === 0 || !older(locator, roomVersion))
|
|
2932
|
+
return new Set();
|
|
2933
|
+
const entries = await local.entries(key, 'confirmed', paths);
|
|
2934
|
+
return new Set(entries.flatMap(({ path, written }) => written !== null &&
|
|
2935
|
+
options.adapter.reached(locator, written, roomVersion) &&
|
|
2936
|
+
!options.adapter.reached(locator, roomVersion, written)
|
|
2937
|
+
? [path]
|
|
2938
|
+
: []));
|
|
2880
2939
|
}
|
|
2881
|
-
function refreshingOf(local, key, liveness) {
|
|
2940
|
+
function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
|
|
2941
|
+
const write = replaying ? finishingWrite : exclusive;
|
|
2882
2942
|
return {
|
|
2943
|
+
turn() {
|
|
2944
|
+
if (replaying)
|
|
2945
|
+
installing(locator);
|
|
2946
|
+
},
|
|
2883
2947
|
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2948
|
+
later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion),
|
|
2884
2949
|
async stage(page) {
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
const last =
|
|
2888
|
-
const
|
|
2950
|
+
for (let written = 0;;) {
|
|
2951
|
+
const end = batchEnd(page.entries, written);
|
|
2952
|
+
const last = end === page.entries.length;
|
|
2953
|
+
const entries = page.entries.slice(written, end);
|
|
2954
|
+
const staged = await write(async () => {
|
|
2889
2955
|
if (!liveness())
|
|
2890
2956
|
return false;
|
|
2891
|
-
await local.refresh(key, last
|
|
2957
|
+
await timed(stageBudget, () => local.refresh(key, composition, last
|
|
2892
2958
|
? { ...page, entries }
|
|
2893
|
-
: { entries, removals: [], folders: [], folderRemovals: [] });
|
|
2959
|
+
: { entries, removals: [], folders: [], folderRemovals: [] }));
|
|
2894
2960
|
return true;
|
|
2895
2961
|
});
|
|
2896
2962
|
if (!staged)
|
|
2897
2963
|
return false;
|
|
2964
|
+
if (last)
|
|
2965
|
+
return true;
|
|
2966
|
+
written = end;
|
|
2898
2967
|
}
|
|
2899
|
-
return true;
|
|
2900
2968
|
},
|
|
2901
2969
|
};
|
|
2902
2970
|
}
|
|
2903
|
-
function
|
|
2904
|
-
|
|
2971
|
+
function batchEnd(entries, from) {
|
|
2972
|
+
let end = from;
|
|
2905
2973
|
let spent = 0;
|
|
2906
|
-
|
|
2907
|
-
const size = (
|
|
2908
|
-
if (spent + size >
|
|
2909
|
-
|
|
2910
|
-
spent = 0;
|
|
2911
|
-
}
|
|
2912
|
-
batches.at(-1).push(entry);
|
|
2974
|
+
while (end < entries.length) {
|
|
2975
|
+
const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
|
|
2976
|
+
if (spent + size > stageBudget.bytes && end > from)
|
|
2977
|
+
break;
|
|
2913
2978
|
spent += size;
|
|
2979
|
+
end += 1;
|
|
2980
|
+
}
|
|
2981
|
+
return end;
|
|
2982
|
+
}
|
|
2983
|
+
async function timed(budget, work) {
|
|
2984
|
+
const started = performance.now();
|
|
2985
|
+
const done = await work();
|
|
2986
|
+
const spent = performance.now() - started;
|
|
2987
|
+
if (spent < budget.aim / 2)
|
|
2988
|
+
budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
|
|
2989
|
+
else if (spent > budget.aim * 2) {
|
|
2990
|
+
budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
|
|
2991
|
+
}
|
|
2992
|
+
return done;
|
|
2993
|
+
}
|
|
2994
|
+
function installWrite(work) {
|
|
2995
|
+
return installTurn(installTurns.starting, work);
|
|
2996
|
+
}
|
|
2997
|
+
function finishingWrite(work) {
|
|
2998
|
+
return installTurn(installTurns.finishing, work);
|
|
2999
|
+
}
|
|
3000
|
+
function installTurn(queue, work) {
|
|
3001
|
+
return new Promise((resolve, reject) => {
|
|
3002
|
+
queue.push(() => void exclusive(work)
|
|
3003
|
+
.then(resolve, reject)
|
|
3004
|
+
.finally(() => {
|
|
3005
|
+
installTurns.busy = false;
|
|
3006
|
+
nextInstallTurn();
|
|
3007
|
+
}));
|
|
3008
|
+
nextInstallTurn();
|
|
3009
|
+
});
|
|
3010
|
+
}
|
|
3011
|
+
function nextInstallTurn() {
|
|
3012
|
+
if (installTurns.busy || installTurns.ahead > 0)
|
|
3013
|
+
return;
|
|
3014
|
+
const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
|
|
3015
|
+
if (turn === undefined)
|
|
3016
|
+
return;
|
|
3017
|
+
installTurns.busy = true;
|
|
3018
|
+
turn();
|
|
3019
|
+
}
|
|
3020
|
+
async function ahead(work) {
|
|
3021
|
+
let taken = false;
|
|
3022
|
+
const hold = {
|
|
3023
|
+
take() {
|
|
3024
|
+
if (taken)
|
|
3025
|
+
return;
|
|
3026
|
+
taken = true;
|
|
3027
|
+
installTurns.ahead += 1;
|
|
3028
|
+
},
|
|
3029
|
+
release() {
|
|
3030
|
+
if (!taken)
|
|
3031
|
+
return;
|
|
3032
|
+
taken = false;
|
|
3033
|
+
installTurns.ahead -= 1;
|
|
3034
|
+
resumeInstall();
|
|
3035
|
+
},
|
|
3036
|
+
};
|
|
3037
|
+
hold.take();
|
|
3038
|
+
try {
|
|
3039
|
+
return await work(hold);
|
|
3040
|
+
}
|
|
3041
|
+
finally {
|
|
3042
|
+
hold.release();
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
function resumeInstall() {
|
|
3046
|
+
if (installTurns.ahead > 0)
|
|
3047
|
+
return;
|
|
3048
|
+
void yielded().then(nextInstallTurn);
|
|
3049
|
+
}
|
|
3050
|
+
async function heldStream() {
|
|
3051
|
+
while (streamsFree === 0)
|
|
3052
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
3053
|
+
streamsFree -= 1;
|
|
3054
|
+
return once(() => {
|
|
3055
|
+
streamsFree += 1;
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
async function unqueued() {
|
|
3059
|
+
while (queuedText >= QUEUED_TEXT) {
|
|
3060
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
2914
3061
|
}
|
|
2915
|
-
return batches;
|
|
2916
3062
|
}
|
|
2917
|
-
|
|
3063
|
+
function once(release) {
|
|
3064
|
+
let released = false;
|
|
3065
|
+
return () => {
|
|
3066
|
+
if (released)
|
|
3067
|
+
return;
|
|
3068
|
+
released = true;
|
|
3069
|
+
release();
|
|
3070
|
+
for (const admit of installWaiters.splice(0))
|
|
3071
|
+
admit();
|
|
3072
|
+
};
|
|
3073
|
+
}
|
|
3074
|
+
function installing(locator) {
|
|
3075
|
+
installs.add(locator);
|
|
3076
|
+
queueReadiness();
|
|
3077
|
+
}
|
|
3078
|
+
function untransferred(locator) {
|
|
3079
|
+
transfers.delete(locator);
|
|
3080
|
+
arrivals.delete(locator);
|
|
3081
|
+
streaming.delete(locator);
|
|
3082
|
+
underived.delete(locator);
|
|
3083
|
+
unindexed.delete(locator);
|
|
3084
|
+
}
|
|
3085
|
+
function setBack(locator) {
|
|
3086
|
+
const now = bytesOf(locator, true);
|
|
3087
|
+
const earlier = startedOver.get(locator);
|
|
3088
|
+
startedOver.set(locator, {
|
|
3089
|
+
done: Math.max(now.done, earlier?.done ?? 0),
|
|
3090
|
+
total: Math.max(now.total, earlier?.total ?? 0),
|
|
3091
|
+
});
|
|
3092
|
+
}
|
|
3093
|
+
function owedIndexes(locator, bytes, text) {
|
|
3094
|
+
if (bytes <= 0 || text <= 0)
|
|
3095
|
+
return;
|
|
3096
|
+
unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
|
|
3097
|
+
}
|
|
3098
|
+
function built(locator, { done, text }) {
|
|
3099
|
+
const owed = unindexed.get(locator) ?? [];
|
|
3100
|
+
let left = text;
|
|
3101
|
+
while (!done && left > 0 && owed.length > 0) {
|
|
3102
|
+
const [first] = owed;
|
|
3103
|
+
if (left >= first.text) {
|
|
3104
|
+
left -= first.text;
|
|
3105
|
+
owed.shift();
|
|
3106
|
+
continue;
|
|
3107
|
+
}
|
|
3108
|
+
owed[0] = {
|
|
3109
|
+
bytes: (first.bytes * (first.text - left)) / first.text,
|
|
3110
|
+
text: first.text - left,
|
|
3111
|
+
};
|
|
3112
|
+
left = 0;
|
|
3113
|
+
}
|
|
3114
|
+
if (done || owed.length === 0)
|
|
3115
|
+
unindexed.delete(locator);
|
|
3116
|
+
queueReadiness();
|
|
3117
|
+
}
|
|
3118
|
+
function installed(locator) {
|
|
3119
|
+
if (installs.delete(locator))
|
|
3120
|
+
queueReadiness();
|
|
3121
|
+
}
|
|
3122
|
+
async function pullRefresh(authorized, held, liveness, hold, refresh) {
|
|
2918
3123
|
if (!liveness())
|
|
2919
3124
|
return unconfirmed;
|
|
2920
|
-
const turn = generation;
|
|
2921
3125
|
const { record } = held;
|
|
2922
|
-
const
|
|
3126
|
+
const from = standingOf(record.locator);
|
|
2923
3127
|
const local = await openLocal();
|
|
2924
3128
|
try {
|
|
2925
3129
|
await exclusive(() => local.unrefresh(record.key));
|
|
2926
|
-
const result = await advancement(
|
|
3130
|
+
const result = await advancement(async (authorization, request) => {
|
|
3131
|
+
hold.release();
|
|
3132
|
+
try {
|
|
3133
|
+
return await refresh(authorization, request);
|
|
3134
|
+
}
|
|
3135
|
+
finally {
|
|
3136
|
+
hold.take();
|
|
3137
|
+
}
|
|
3138
|
+
}, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness));
|
|
2927
3139
|
if (result.kind !== 'advanced') {
|
|
2928
3140
|
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2929
3141
|
return result;
|
|
2930
3142
|
}
|
|
2931
|
-
const refused = await validatedRoom(local, result.record, 'refreshed', {
|
|
2932
|
-
record,
|
|
2933
|
-
changed: [...result.changed],
|
|
2934
|
-
});
|
|
2935
|
-
if (refused !== null) {
|
|
2936
|
-
journal.record(`transfer refresh ${record.locator} refused`);
|
|
2937
|
-
return refused;
|
|
2938
|
-
}
|
|
2939
3143
|
journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
|
|
2940
|
-
return {
|
|
3144
|
+
return {
|
|
3145
|
+
kind: 'advanced',
|
|
3146
|
+
record: result.record,
|
|
3147
|
+
from,
|
|
3148
|
+
remodelled: result.record.model !== record.model,
|
|
3149
|
+
};
|
|
2941
3150
|
}
|
|
2942
3151
|
catch (error) {
|
|
3152
|
+
if (refusedForSpace(error))
|
|
3153
|
+
return unconfirmed;
|
|
2943
3154
|
journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
|
|
2944
3155
|
throw error;
|
|
2945
3156
|
}
|
|
2946
|
-
finally {
|
|
2947
|
-
reads.finished(record.locator, turn);
|
|
2948
|
-
}
|
|
2949
3157
|
}
|
|
2950
3158
|
const heldContents = new Map();
|
|
2951
3159
|
function heldContentOf(local, key) {
|
|
@@ -2963,63 +3171,115 @@ export function composeRuntime(options) {
|
|
|
2963
3171
|
heldContents.set(key, made);
|
|
2964
3172
|
return made;
|
|
2965
3173
|
}
|
|
3174
|
+
async function abandoned(local, locator) {
|
|
3175
|
+
await local.abandon(locator);
|
|
3176
|
+
setBack(locator);
|
|
3177
|
+
untransferred(locator);
|
|
3178
|
+
liveQueries.recover();
|
|
3179
|
+
sweep();
|
|
3180
|
+
}
|
|
2966
3181
|
function staging(local, liveness) {
|
|
3182
|
+
const derived = async (locator, key, composition) => {
|
|
3183
|
+
underived.delete(locator);
|
|
3184
|
+
for (;;) {
|
|
3185
|
+
if (!liveness())
|
|
3186
|
+
return false;
|
|
3187
|
+
const done = await finishingWrite(async () => liveness()
|
|
3188
|
+
? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
|
|
3189
|
+
: null);
|
|
3190
|
+
if (done === null)
|
|
3191
|
+
return false;
|
|
3192
|
+
built(locator, done);
|
|
3193
|
+
if (done.done)
|
|
3194
|
+
return true;
|
|
3195
|
+
}
|
|
3196
|
+
};
|
|
2967
3197
|
return {
|
|
2968
3198
|
held: (locator) => transfers.get(locator) ?? null,
|
|
3199
|
+
async turn(locator, step) {
|
|
3200
|
+
installing(locator);
|
|
3201
|
+
return step === 'downloading' ? heldStream() : () => { };
|
|
3202
|
+
},
|
|
3203
|
+
unqueued,
|
|
3204
|
+
queued(text) {
|
|
3205
|
+
queuedText += text;
|
|
3206
|
+
return once(() => {
|
|
3207
|
+
queuedText -= text;
|
|
3208
|
+
});
|
|
3209
|
+
},
|
|
3210
|
+
arriving(locator) {
|
|
3211
|
+
let ended = false;
|
|
3212
|
+
return {
|
|
3213
|
+
at(bytes) {
|
|
3214
|
+
if (ended)
|
|
3215
|
+
return;
|
|
3216
|
+
arrivals.set(locator, bytes);
|
|
3217
|
+
streaming.add(locator);
|
|
3218
|
+
const now = performance.now();
|
|
3219
|
+
if (now - arrivalReported < ARRIVAL_REPORT_MS)
|
|
3220
|
+
return;
|
|
3221
|
+
arrivalReported = now;
|
|
3222
|
+
queueReadiness();
|
|
3223
|
+
},
|
|
3224
|
+
end() {
|
|
3225
|
+
if (ended)
|
|
3226
|
+
return;
|
|
3227
|
+
ended = true;
|
|
3228
|
+
if (streaming.delete(locator))
|
|
3229
|
+
queueReadiness();
|
|
3230
|
+
},
|
|
3231
|
+
};
|
|
3232
|
+
},
|
|
3233
|
+
streamed: options.adapter.roomStream !== undefined,
|
|
2969
3234
|
async stage(record, page, composition) {
|
|
2970
|
-
const
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
if (!liveness())
|
|
2977
|
-
return null;
|
|
2978
|
-
if (index === 0 && !(await admitted(local, record.locator, bytes)))
|
|
2979
|
-
return null;
|
|
2980
|
-
try {
|
|
2981
|
-
return await local.stage(record, composition, { entries, folders: last ? page.folders : [] }, key, last);
|
|
2982
|
-
}
|
|
2983
|
-
catch (error) {
|
|
2984
|
-
if (!exceeded(error))
|
|
2985
|
-
throw error;
|
|
2986
|
-
await exceededOn(record.locator, bytes);
|
|
2987
|
-
return null;
|
|
2988
|
-
}
|
|
2989
|
-
});
|
|
2990
|
-
if (staged === null)
|
|
3235
|
+
const { locator } = record;
|
|
3236
|
+
installing(locator);
|
|
3237
|
+
if (!liveness())
|
|
3238
|
+
return null;
|
|
3239
|
+
const key = await installWrite(async () => {
|
|
3240
|
+
if (!liveness())
|
|
2991
3241
|
return null;
|
|
2992
|
-
|
|
2993
|
-
}
|
|
2994
|
-
|
|
3242
|
+
return local.stage(record, composition, page);
|
|
3243
|
+
});
|
|
3244
|
+
if (key === null || !liveness())
|
|
3245
|
+
return null;
|
|
3246
|
+
const arrived = !transfers.has(locator);
|
|
3247
|
+
const prior = transfers.get(locator)?.done.bytes ?? 0;
|
|
3248
|
+
const held = { ...record, key };
|
|
3249
|
+
transfers.set(locator, held);
|
|
3250
|
+
if (arrived)
|
|
3251
|
+
liveQueries.recover();
|
|
3252
|
+
installing(locator);
|
|
2995
3253
|
liveQueries.recover();
|
|
2996
|
-
|
|
3254
|
+
const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
|
|
3255
|
+
owedIndexes(locator, record.done.bytes - prior, text);
|
|
3256
|
+
const owed = text + (underived.get(locator) ?? 0);
|
|
3257
|
+
underived.set(locator, owed);
|
|
3258
|
+
if (owed < deriveBudget.bytes)
|
|
3259
|
+
return held;
|
|
3260
|
+
return (await derived(locator, key, composition)) ? held : null;
|
|
2997
3261
|
},
|
|
2998
|
-
|
|
3262
|
+
derive: derived,
|
|
3263
|
+
async refreshing(locator, key, composition) {
|
|
2999
3264
|
await exclusive(() => local.unrefresh(key));
|
|
3000
|
-
return refreshingOf(local, key, liveness);
|
|
3265
|
+
return refreshingOf(local, locator, key, composition, liveness, true);
|
|
3001
3266
|
},
|
|
3002
3267
|
abandon: (locator) => exclusive(async () => {
|
|
3003
|
-
if (
|
|
3004
|
-
|
|
3005
|
-
await local.abandon(locator);
|
|
3006
|
-
transfers.delete(locator);
|
|
3007
|
-
liveQueries.recover();
|
|
3008
|
-
sweep();
|
|
3268
|
+
if (liveness())
|
|
3269
|
+
await abandoned(local, locator);
|
|
3009
3270
|
}),
|
|
3010
|
-
setAside:
|
|
3271
|
+
setAside: async (locator) => {
|
|
3011
3272
|
if (!liveness())
|
|
3012
3273
|
return;
|
|
3013
|
-
|
|
3014
|
-
|
|
3274
|
+
setBack(locator);
|
|
3275
|
+
untransferred(locator);
|
|
3015
3276
|
liveQueries.recover();
|
|
3016
|
-
}
|
|
3277
|
+
},
|
|
3017
3278
|
release: (key) => exclusive(async () => {
|
|
3018
3279
|
await local.release(key);
|
|
3019
3280
|
sweep();
|
|
3020
3281
|
}),
|
|
3021
3282
|
content: (key) => heldContentOf(local, key),
|
|
3022
|
-
validate: (record, view) => validatedRoom(local, record, view, null),
|
|
3023
3283
|
};
|
|
3024
3284
|
}
|
|
3025
3285
|
function reusable(room, local) {
|
|
@@ -3031,30 +3291,35 @@ export function composeRuntime(options) {
|
|
|
3031
3291
|
async function pullBootstrap(authorized, room, local, liveness) {
|
|
3032
3292
|
if (!liveness())
|
|
3033
3293
|
return unconfirmed;
|
|
3034
|
-
const turn = generation;
|
|
3035
3294
|
if (replicas.has(room.locator)) {
|
|
3036
3295
|
catching.add(room.locator);
|
|
3037
3296
|
liveQueries.recover();
|
|
3038
3297
|
}
|
|
3039
3298
|
try {
|
|
3040
|
-
const result = await bootstrapRoom(reads, authorized, room, {
|
|
3041
|
-
collection: modelOf(room.locator),
|
|
3042
|
-
selection: modelOf(room.locator, 'typed-selection'),
|
|
3043
|
-
}, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
|
|
3299
|
+
const result = await bootstrapRoom(reads, authorized, room, { collection: modelOf(room.locator) }, builtIn, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
|
|
3044
3300
|
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
3045
3301
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
3046
|
-
if (result.kind
|
|
3302
|
+
if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
|
|
3047
3303
|
caughtUpTo(room.locator);
|
|
3304
|
+
installed(room.locator);
|
|
3305
|
+
await abandoned(local, room.locator);
|
|
3306
|
+
journal.record(`transfer bootstrap ${room.locator} older than shown`);
|
|
3307
|
+
return { kind: 'transport-uncertainty' };
|
|
3308
|
+
}
|
|
3309
|
+
if (result.kind !== 'staged') {
|
|
3310
|
+
caughtUpTo(room.locator);
|
|
3311
|
+
installed(room.locator);
|
|
3312
|
+
}
|
|
3048
3313
|
return result;
|
|
3049
3314
|
}
|
|
3050
3315
|
catch (error) {
|
|
3051
|
-
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
3052
3316
|
caughtUpTo(room.locator);
|
|
3317
|
+
installed(room.locator);
|
|
3318
|
+
if (refusedForSpace(error))
|
|
3319
|
+
return unconfirmed;
|
|
3320
|
+
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
3053
3321
|
throw error;
|
|
3054
3322
|
}
|
|
3055
|
-
finally {
|
|
3056
|
-
reads.finished(room.locator, turn);
|
|
3057
|
-
}
|
|
3058
3323
|
}
|
|
3059
3324
|
function caughtUpTo(locator) {
|
|
3060
3325
|
if (catching.delete(locator))
|
|
@@ -3062,72 +3327,83 @@ export function composeRuntime(options) {
|
|
|
3062
3327
|
}
|
|
3063
3328
|
function behind(room) {
|
|
3064
3329
|
const held = replicas.get(room.locator)?.record.roomVersion;
|
|
3065
|
-
return
|
|
3066
|
-
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
3330
|
+
return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
|
|
3067
3331
|
}
|
|
3068
|
-
|
|
3069
|
-
const
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
return
|
|
3332
|
+
function standingOf(locator) {
|
|
3333
|
+
const held = replicas.get(locator);
|
|
3334
|
+
return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
|
|
3335
|
+
}
|
|
3336
|
+
function stands(locator, from) {
|
|
3337
|
+
const now = standingOf(locator);
|
|
3338
|
+
return now.key === from.key && now.confirmed === from.confirmed;
|
|
3075
3339
|
}
|
|
3076
|
-
async function
|
|
3077
|
-
|
|
3340
|
+
async function advance(room, local, authorized, running) {
|
|
3341
|
+
const live = () => running() && !full;
|
|
3342
|
+
if (!live())
|
|
3078
3343
|
return unconfirmed;
|
|
3079
3344
|
const held = replicas.get(room.locator);
|
|
3080
|
-
if (held !== undefined) {
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
emitRooms();
|
|
3092
|
-
}
|
|
3093
|
-
return null;
|
|
3094
|
-
}
|
|
3095
|
-
if (refreshed.kind === 'advanced') {
|
|
3096
|
-
const landing = { kind: 'refreshed', base: refreshed.base };
|
|
3097
|
-
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3098
|
-
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3099
|
-
const reached = now !== undefined &&
|
|
3100
|
-
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3101
|
-
return reached ? null : advance(room, local, authorized, live);
|
|
3102
|
-
}
|
|
3103
|
-
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
3104
|
-
return null;
|
|
3105
|
-
}
|
|
3106
|
-
if (refreshed.kind !== 'beyond-horizon')
|
|
3345
|
+
if (held !== undefined && options.adapter.roomStream !== undefined) {
|
|
3346
|
+
stale.delete(room.locator);
|
|
3347
|
+
name(room.locator);
|
|
3348
|
+
return null;
|
|
3349
|
+
}
|
|
3350
|
+
const refresh = reads.refresh;
|
|
3351
|
+
if (held !== undefined && refresh !== undefined) {
|
|
3352
|
+
const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
|
|
3353
|
+
if (refreshed === 'behind')
|
|
3354
|
+
return advance(room, local, authorized, live);
|
|
3355
|
+
if (refreshed !== 'beyond-horizon')
|
|
3107
3356
|
return refreshed;
|
|
3108
|
-
if (
|
|
3109
|
-
owed.add(room.locator);
|
|
3110
|
-
liveQueries.recover();
|
|
3111
|
-
return unconfirmed;
|
|
3112
|
-
}
|
|
3113
|
-
if (reloadsDue.get(room.locator) === generation)
|
|
3357
|
+
if (reloadsDue.has(room.locator))
|
|
3114
3358
|
return unconfirmed;
|
|
3359
|
+
replaced([room.locator]);
|
|
3115
3360
|
}
|
|
3361
|
+
const from = standingOf(room.locator);
|
|
3116
3362
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
3363
|
+
if (bootstrapped.kind !== 'staged')
|
|
3364
|
+
return live() ? bootstrapped : unconfirmed;
|
|
3365
|
+
const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
|
|
3366
|
+
if (await publish(local, room, bootstrapped.record, landing, live)) {
|
|
3367
|
+
return live() ? null : unconfirmed;
|
|
3368
|
+
}
|
|
3369
|
+
return reachedBy(room.locator, bootstrapped.record.roomVersion)
|
|
3370
|
+
? null
|
|
3371
|
+
: advance(room, local, authorized, live);
|
|
3372
|
+
}
|
|
3373
|
+
async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
|
|
3374
|
+
const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
|
|
3117
3375
|
if (!live())
|
|
3118
3376
|
return unconfirmed;
|
|
3119
|
-
if (
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3377
|
+
if (refreshed.kind === 'still-current') {
|
|
3378
|
+
stale.delete(room.locator);
|
|
3379
|
+
if (unreadable.delete(room.locator)) {
|
|
3380
|
+
scopedRefusals.delete(room.locator);
|
|
3381
|
+
list(room, true);
|
|
3382
|
+
emitRooms();
|
|
3383
|
+
}
|
|
3384
|
+
return null;
|
|
3385
|
+
}
|
|
3386
|
+
if (refreshed.kind === 'advanced') {
|
|
3387
|
+
const landing = {
|
|
3388
|
+
kind: 'refreshed',
|
|
3389
|
+
from: refreshed.from,
|
|
3390
|
+
remodelled: refreshed.remodelled,
|
|
3391
|
+
};
|
|
3392
|
+
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3393
|
+
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3394
|
+
const reached = now !== undefined &&
|
|
3395
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3396
|
+
return reached ? null : 'behind';
|
|
3397
|
+
}
|
|
3398
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3399
|
+
return null;
|
|
3400
|
+
}
|
|
3401
|
+
return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
|
|
3123
3402
|
}
|
|
3124
3403
|
async function withhold(withheld, room, local, progress) {
|
|
3125
|
-
if (withheld.kind === 'unconfirmed')
|
|
3404
|
+
if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
|
|
3126
3405
|
return;
|
|
3127
|
-
|
|
3128
|
-
stale.delete(room.locator);
|
|
3129
|
-
else
|
|
3130
|
-
stale.add(room.locator);
|
|
3406
|
+
stale.add(room.locator);
|
|
3131
3407
|
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
3132
3408
|
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
3133
3409
|
repairable.set(room.locator, reset);
|
|
@@ -3149,30 +3425,10 @@ export function composeRuntime(options) {
|
|
|
3149
3425
|
list(room, false);
|
|
3150
3426
|
emitRooms();
|
|
3151
3427
|
}
|
|
3152
|
-
else if (withheld.kind === 'refused') {
|
|
3153
|
-
refusedSource(room, withheld);
|
|
3154
|
-
}
|
|
3155
3428
|
else if (withheld.kind === 'revoked-authorization') {
|
|
3156
3429
|
await refuse('revoked-authorization');
|
|
3157
3430
|
}
|
|
3158
3431
|
}
|
|
3159
|
-
function refusedSource(room, { refusal }) {
|
|
3160
|
-
sourceRefusals.set(room.locator, {
|
|
3161
|
-
code: refusal.rule,
|
|
3162
|
-
retryable: false,
|
|
3163
|
-
fields: {
|
|
3164
|
-
file: refusal.file,
|
|
3165
|
-
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
3166
|
-
...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
|
|
3167
|
-
},
|
|
3168
|
-
});
|
|
3169
|
-
declarationRefusals.delete(room.locator);
|
|
3170
|
-
unreadable.add(room.locator);
|
|
3171
|
-
list(room, false);
|
|
3172
|
-
emitRooms();
|
|
3173
|
-
moveTo({ ...status, ...attention() });
|
|
3174
|
-
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
3175
|
-
}
|
|
3176
3432
|
function leaving(batch, result) {
|
|
3177
3433
|
if (result.kind === 'expired')
|
|
3178
3434
|
return { batch, returned: returnedBy('expired') };
|
|
@@ -3194,8 +3450,8 @@ export function composeRuntime(options) {
|
|
|
3194
3450
|
function settling(acceptance) {
|
|
3195
3451
|
return acceptance !== null && 'apply' in acceptance;
|
|
3196
3452
|
}
|
|
3197
|
-
function
|
|
3198
|
-
return settling(acceptance) ?
|
|
3453
|
+
function failureOf(acceptance) {
|
|
3454
|
+
return settling(acceptance) ? null : acceptance;
|
|
3199
3455
|
}
|
|
3200
3456
|
async function acceptance(room, local, submitted, accepted) {
|
|
3201
3457
|
if (!continuing())
|
|
@@ -3208,7 +3464,8 @@ export function composeRuntime(options) {
|
|
|
3208
3464
|
logAccepted(room.locator, submitted, accepted);
|
|
3209
3465
|
let record = current.record;
|
|
3210
3466
|
let changes;
|
|
3211
|
-
if (!options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
3467
|
+
if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
|
|
3468
|
+
!older(room.locator, accepted.roomVersion)) {
|
|
3212
3469
|
const paths = [...returnedPaths(submitted, accepted)];
|
|
3213
3470
|
const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
|
|
3214
3471
|
const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
|
|
@@ -3218,34 +3475,6 @@ export function composeRuntime(options) {
|
|
|
3218
3475
|
? accepted.roomVersion
|
|
3219
3476
|
: held,
|
|
3220
3477
|
};
|
|
3221
|
-
const overrides = new Map([
|
|
3222
|
-
...back.set.map((entry) => [entry.path, entry]),
|
|
3223
|
-
...back.removed.map((path) => [path, null]),
|
|
3224
|
-
]);
|
|
3225
|
-
const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
|
|
3226
|
-
if (refused !== null) {
|
|
3227
|
-
const key = pendingKey(room.locator, submitted.id);
|
|
3228
|
-
return {
|
|
3229
|
-
locator: room.locator,
|
|
3230
|
-
left: [],
|
|
3231
|
-
commit: {
|
|
3232
|
-
rooms: [],
|
|
3233
|
-
writes: [
|
|
3234
|
-
{
|
|
3235
|
-
locator: room.locator,
|
|
3236
|
-
accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
|
|
3237
|
-
},
|
|
3238
|
-
],
|
|
3239
|
-
},
|
|
3240
|
-
refused,
|
|
3241
|
-
apply() {
|
|
3242
|
-
const state = pendings.get(key);
|
|
3243
|
-
if (state !== undefined && !fences.has(room.locator))
|
|
3244
|
-
pendings.set(key, { ...state, accepted: accepted.roomVersion });
|
|
3245
|
-
return changeSet(room.locator, { queue: true });
|
|
3246
|
-
},
|
|
3247
|
-
};
|
|
3248
|
-
}
|
|
3249
3478
|
changes = back;
|
|
3250
3479
|
}
|
|
3251
3480
|
const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
@@ -3253,10 +3482,9 @@ export function composeRuntime(options) {
|
|
|
3253
3482
|
...settled,
|
|
3254
3483
|
apply(answer) {
|
|
3255
3484
|
const set = settled.apply(answer);
|
|
3485
|
+
showing(room.locator, accepted.roomVersion);
|
|
3256
3486
|
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3257
3487
|
unreadable.delete(room.locator);
|
|
3258
|
-
sourceRefusals.delete(room.locator);
|
|
3259
|
-
declarationRefusals.delete(room.locator);
|
|
3260
3488
|
list(room, true);
|
|
3261
3489
|
return set;
|
|
3262
3490
|
},
|
|
@@ -3274,13 +3502,9 @@ export function composeRuntime(options) {
|
|
|
3274
3502
|
const failed = await exclusive(async () => {
|
|
3275
3503
|
const adopting = await acceptance(room, local, submitted, accepted);
|
|
3276
3504
|
await adoptedTogether(local, [adopting]);
|
|
3277
|
-
return
|
|
3505
|
+
return failureOf(adopting);
|
|
3278
3506
|
});
|
|
3279
|
-
|
|
3280
|
-
return true;
|
|
3281
|
-
if (failed.kind === 'refused' && continuing())
|
|
3282
|
-
refusedSource(room, failed);
|
|
3283
|
-
return false;
|
|
3507
|
+
return failed === null;
|
|
3284
3508
|
}
|
|
3285
3509
|
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
3286
3510
|
const other = discovered.get(partner);
|
|
@@ -3294,18 +3518,10 @@ export function composeRuntime(options) {
|
|
|
3294
3518
|
...accepted.destination,
|
|
3295
3519
|
});
|
|
3296
3520
|
await adoptedTogether(local, [left, arrived]);
|
|
3297
|
-
|
|
3298
|
-
if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
|
|
3299
|
-
refusedSource(other, refusedArrival);
|
|
3300
|
-
}
|
|
3301
|
-
return refusalOf(left);
|
|
3521
|
+
return failureOf(left);
|
|
3302
3522
|
});
|
|
3303
3523
|
submitLater(partner);
|
|
3304
|
-
|
|
3305
|
-
return true;
|
|
3306
|
-
if (failed.kind === 'refused' && continuing())
|
|
3307
|
-
refusedSource(room, failed);
|
|
3308
|
-
return false;
|
|
3524
|
+
return failed === null;
|
|
3309
3525
|
}
|
|
3310
3526
|
function queueIn(locator) {
|
|
3311
3527
|
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
@@ -3424,12 +3640,10 @@ export function composeRuntime(options) {
|
|
|
3424
3640
|
async function submitRoom(room, local, authorized, progress, signal) {
|
|
3425
3641
|
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
3426
3642
|
const replica = replicas.get(room.locator);
|
|
3427
|
-
const head = replica?.queue
|
|
3643
|
+
const head = replica?.queue.find((batch) => !landedAhead.has(pendingKey(room.locator, batch.id)));
|
|
3428
3644
|
if (replica === undefined || head === undefined)
|
|
3429
3645
|
return 'done';
|
|
3430
3646
|
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
3431
|
-
if (state?.accepted !== undefined)
|
|
3432
|
-
return 'done';
|
|
3433
3647
|
const partner = partnerOf(head, room.locator);
|
|
3434
3648
|
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3435
3649
|
submitLater(partner);
|
|
@@ -3543,12 +3757,12 @@ export function composeRuntime(options) {
|
|
|
3543
3757
|
}
|
|
3544
3758
|
if (result.kind === 'expired') {
|
|
3545
3759
|
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
3546
|
-
const there = other === undefined ? null : await
|
|
3760
|
+
const there = other === undefined ? null : await advanced(other, local, authorized);
|
|
3547
3761
|
if (other !== undefined && there !== null) {
|
|
3548
3762
|
await withhold(there, other, local, progress);
|
|
3549
3763
|
return 'done';
|
|
3550
3764
|
}
|
|
3551
|
-
const withheld = await
|
|
3765
|
+
const withheld = await advanced(room, local, authorized);
|
|
3552
3766
|
if (withheld !== null) {
|
|
3553
3767
|
await withhold(withheld, room, local, progress);
|
|
3554
3768
|
return 'done';
|
|
@@ -3570,8 +3784,8 @@ export function composeRuntime(options) {
|
|
|
3570
3784
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
3571
3785
|
const left = leaving(head, result);
|
|
3572
3786
|
const refreshed = async () => ({
|
|
3573
|
-
there: other === undefined ? null : await
|
|
3574
|
-
here: await
|
|
3787
|
+
there: other === undefined ? null : await advanced(other, local, authorized),
|
|
3788
|
+
here: await advanced(room, local, authorized),
|
|
3575
3789
|
});
|
|
3576
3790
|
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
3577
3791
|
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
@@ -3595,6 +3809,13 @@ export function composeRuntime(options) {
|
|
|
3595
3809
|
(await landedWhole(room.locator, head, confirmedLayer))) ||
|
|
3596
3810
|
(result.kind === 'conflicted' &&
|
|
3597
3811
|
(await landedWhole(room.locator, head, confirmedLayer, result.affected)));
|
|
3812
|
+
if (held &&
|
|
3813
|
+
result.kind === 'conflicted' &&
|
|
3814
|
+
partner === null &&
|
|
3815
|
+
!landed(head, confirmedLayer)) {
|
|
3816
|
+
landedAhead.set(pendingKey(room.locator, head.id), Object.fromEntries([...touchedBy([head])].map((path) => [path, confirmedLayer.revisions[path]])));
|
|
3817
|
+
return;
|
|
3818
|
+
}
|
|
3598
3819
|
publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
|
|
3599
3820
|
await unpair(local);
|
|
3600
3821
|
moveTo({ ...status, ...attention() });
|
|
@@ -3642,6 +3863,7 @@ export function composeRuntime(options) {
|
|
|
3642
3863
|
locator: room.locator,
|
|
3643
3864
|
key: before.record.key,
|
|
3644
3865
|
roots: before.record.typedSelection?.roots ?? [],
|
|
3866
|
+
composed: composedOf(composed(before.record)),
|
|
3645
3867
|
overlay: { set: [], cleared: [...before.overlaid] },
|
|
3646
3868
|
},
|
|
3647
3869
|
]),
|
|
@@ -3650,8 +3872,10 @@ export function composeRuntime(options) {
|
|
|
3650
3872
|
for (const { room, before, queue } of held) {
|
|
3651
3873
|
const { locator } = room;
|
|
3652
3874
|
lastFenced.set(locator, ++fencings);
|
|
3653
|
-
for (const batch of queue)
|
|
3875
|
+
for (const batch of queue) {
|
|
3654
3876
|
pendings.delete(pendingKey(locator, batch.id));
|
|
3877
|
+
landedAhead.delete(pendingKey(locator, batch.id));
|
|
3878
|
+
}
|
|
3655
3879
|
queuesWithoutReplica.delete(locator);
|
|
3656
3880
|
if (before !== undefined) {
|
|
3657
3881
|
replicas.set(locator, {
|
|
@@ -3674,39 +3898,61 @@ export function composeRuntime(options) {
|
|
|
3674
3898
|
if (signal.aborted)
|
|
3675
3899
|
return;
|
|
3676
3900
|
const progress = { complete: true, reachable: true };
|
|
3677
|
-
await roomTask(room.locator,
|
|
3678
|
-
await Promise.race([
|
|
3679
|
-
reload(room, local, progress, () => live() && !signal.aborted),
|
|
3680
|
-
abandonedOn(signal),
|
|
3681
|
-
]);
|
|
3682
|
-
});
|
|
3901
|
+
await roomTask(room.locator, (live) => reload(room, local, progress, live));
|
|
3683
3902
|
if (signal.aborted)
|
|
3684
3903
|
return;
|
|
3685
3904
|
await rest(progress);
|
|
3686
3905
|
}
|
|
3687
3906
|
}
|
|
3688
3907
|
function awaitingAnswer(locator) {
|
|
3908
|
+
answering.get(locator)?.release();
|
|
3689
3909
|
let release = () => { };
|
|
3690
|
-
const
|
|
3910
|
+
const answered = new Promise((resolve) => {
|
|
3691
3911
|
release = resolve;
|
|
3692
3912
|
});
|
|
3693
|
-
|
|
3913
|
+
const awaited = { answered, release };
|
|
3914
|
+
answering.set(locator, awaited);
|
|
3694
3915
|
return () => {
|
|
3695
|
-
if (answering.get(locator)
|
|
3916
|
+
if (answering.get(locator) === awaited)
|
|
3696
3917
|
answering.delete(locator);
|
|
3697
3918
|
release();
|
|
3698
3919
|
};
|
|
3699
3920
|
}
|
|
3700
|
-
|
|
3701
|
-
const
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3921
|
+
function declaredAsHeld(room) {
|
|
3922
|
+
const held = replicas.get(room.locator)?.record;
|
|
3923
|
+
return (held === undefined ||
|
|
3924
|
+
(JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
|
|
3925
|
+
JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
|
|
3926
|
+
JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
|
|
3927
|
+
}
|
|
3928
|
+
async function keepNaming(local, room) {
|
|
3929
|
+
const held = replicas.get(room.locator)?.record;
|
|
3930
|
+
if (held === undefined || (held.name === room.name && held.handle === room.handle))
|
|
3931
|
+
return;
|
|
3932
|
+
await exclusive(async () => {
|
|
3933
|
+
if (!continuing())
|
|
3934
|
+
return;
|
|
3935
|
+
const replica = replicas.get(room.locator);
|
|
3936
|
+
if (replica === undefined)
|
|
3937
|
+
return;
|
|
3938
|
+
const record = { ...replica.record, name: room.name, handle: room.handle };
|
|
3939
|
+
await local.commit({
|
|
3940
|
+
rooms: [
|
|
3941
|
+
{
|
|
3942
|
+
locator: room.locator,
|
|
3943
|
+
key: record.key,
|
|
3944
|
+
record: storedRecord(record),
|
|
3945
|
+
composed: composedOf(composed(record)),
|
|
3946
|
+
},
|
|
3947
|
+
],
|
|
3948
|
+
});
|
|
3949
|
+
replicas.set(room.locator, { ...replica, record });
|
|
3950
|
+
});
|
|
3951
|
+
}
|
|
3952
|
+
async function retainModel(local, room, next) {
|
|
3953
|
+
const key = room.locator;
|
|
3954
|
+
const previous = modelOf(room.locator);
|
|
3955
|
+
if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
|
|
3710
3956
|
return;
|
|
3711
3957
|
if (next.status === 'unavailable') {
|
|
3712
3958
|
invalidModels.add(key);
|
|
@@ -3718,26 +3964,15 @@ export function composeRuntime(options) {
|
|
|
3718
3964
|
const held = replicas.get(room.locator);
|
|
3719
3965
|
let changed = null;
|
|
3720
3966
|
if (held !== undefined) {
|
|
3721
|
-
const
|
|
3967
|
+
const record = {
|
|
3722
3968
|
...held.record,
|
|
3969
|
+
name: room.name,
|
|
3970
|
+
handle: room.handle,
|
|
3723
3971
|
operations: room.operations,
|
|
3724
3972
|
replica: room.replica,
|
|
3725
3973
|
typedSelection: room.typedSelection ?? null,
|
|
3726
|
-
|
|
3974
|
+
model: next,
|
|
3727
3975
|
};
|
|
3728
|
-
let read = 'staged';
|
|
3729
|
-
if (!sameMembership(declared, held.record)) {
|
|
3730
|
-
const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
|
|
3731
|
-
read =
|
|
3732
|
-
verdict.verdict !== 'refuse'
|
|
3733
|
-
? 'staged'
|
|
3734
|
-
: verdict.rule === 'model-unavailable'
|
|
3735
|
-
? 'deferred'
|
|
3736
|
-
: { kind: 'refused', refusal: verdict };
|
|
3737
|
-
}
|
|
3738
|
-
const record = read === 'staged'
|
|
3739
|
-
? declared
|
|
3740
|
-
: { ...declared, typedSelection: held.record.typedSelection };
|
|
3741
3976
|
const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
|
|
3742
3977
|
const replayedLayer = await replayed(record, confirmedLayer, held.queue);
|
|
3743
3978
|
const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
|
|
@@ -3748,41 +3983,33 @@ export function composeRuntime(options) {
|
|
|
3748
3983
|
key: record.key,
|
|
3749
3984
|
record: storedRecord(record),
|
|
3750
3985
|
roots: record.typedSelection?.roots ?? [],
|
|
3751
|
-
|
|
3986
|
+
composed: composedOf(composed(record)),
|
|
3752
3987
|
overlay: diff.overlay,
|
|
3753
3988
|
},
|
|
3754
3989
|
],
|
|
3755
3990
|
});
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count });
|
|
3991
|
+
const { count, indexed } = committedRoom(room.locator, answer);
|
|
3992
|
+
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
|
|
3759
3993
|
changed = changeSet(room.locator, {
|
|
3760
3994
|
documents: answer.changed[room.locator] ?? [],
|
|
3761
3995
|
room: true,
|
|
3762
3996
|
});
|
|
3763
|
-
if (read === 'staged' || read === 'deferred') {
|
|
3764
|
-
if (declarationRefusals.delete(room.locator)) {
|
|
3765
|
-
sourceRefusals.delete(room.locator);
|
|
3766
|
-
unreadable.delete(room.locator);
|
|
3767
|
-
}
|
|
3768
|
-
}
|
|
3769
|
-
else {
|
|
3770
|
-
refusedSource(room, read);
|
|
3771
|
-
declarationRefusals.add(room.locator);
|
|
3772
|
-
}
|
|
3773
|
-
}
|
|
3774
|
-
models.set(key, next);
|
|
3775
|
-
if (next.status === 'current') {
|
|
3776
|
-
liveModels.set(key, JSON.stringify(next.snapshot));
|
|
3777
|
-
invalidModels.delete(key);
|
|
3778
3997
|
}
|
|
3779
|
-
|
|
3998
|
+
heldModel(room, next);
|
|
3780
3999
|
if (changed === null)
|
|
3781
4000
|
emitRooms();
|
|
3782
4001
|
else
|
|
3783
4002
|
publishChangeSet(changed);
|
|
3784
4003
|
});
|
|
3785
4004
|
}
|
|
4005
|
+
function heldModel(room, next) {
|
|
4006
|
+
models.set(room.locator, next);
|
|
4007
|
+
if (next.status === 'current') {
|
|
4008
|
+
liveModels.set(room.locator, JSON.stringify(next.snapshot));
|
|
4009
|
+
invalidModels.delete(room.locator);
|
|
4010
|
+
}
|
|
4011
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
4012
|
+
}
|
|
3786
4013
|
async function scoped(result, room, local) {
|
|
3787
4014
|
if (result.scope === 'content' && !result.retryable) {
|
|
3788
4015
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
@@ -3796,14 +4023,14 @@ export function composeRuntime(options) {
|
|
|
3796
4023
|
});
|
|
3797
4024
|
let scopedAway = room;
|
|
3798
4025
|
if (result.scope === 'model')
|
|
3799
|
-
await retainModel(local, room,
|
|
4026
|
+
await retainModel(local, room, {
|
|
3800
4027
|
status: 'unavailable',
|
|
3801
4028
|
reason: 'not-readable',
|
|
3802
4029
|
});
|
|
3803
4030
|
else if (result.scope !== 'operation') {
|
|
3804
4031
|
unreadable.add(room.locator);
|
|
3805
4032
|
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
3806
|
-
await retainModel(local, scopedAway,
|
|
4033
|
+
await retainModel(local, scopedAway, modelOf(room.locator));
|
|
3807
4034
|
}
|
|
3808
4035
|
if (!continuing())
|
|
3809
4036
|
return;
|
|
@@ -3811,6 +4038,17 @@ export function composeRuntime(options) {
|
|
|
3811
4038
|
emitRooms();
|
|
3812
4039
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
3813
4040
|
}
|
|
4041
|
+
function name(locator) {
|
|
4042
|
+
const stream = options.adapter.roomStream;
|
|
4043
|
+
const record = replicas.get(locator)?.record;
|
|
4044
|
+
const room = discovered.get(locator);
|
|
4045
|
+
if (stream === undefined || record === undefined || room === undefined)
|
|
4046
|
+
return;
|
|
4047
|
+
if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
|
|
4048
|
+
return;
|
|
4049
|
+
naming += 1;
|
|
4050
|
+
stream.hold([{ locator, roomVersion: record.roomVersion }]);
|
|
4051
|
+
}
|
|
3814
4052
|
function heldRooms(listed) {
|
|
3815
4053
|
return listed.flatMap((room) => {
|
|
3816
4054
|
const record = replicas.get(room.locator)?.record;
|
|
@@ -3828,14 +4066,19 @@ export function composeRuntime(options) {
|
|
|
3828
4066
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
3829
4067
|
});
|
|
3830
4068
|
if (attempt === 'opened') {
|
|
4069
|
+
naming += 1;
|
|
3831
4070
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
3832
4071
|
handed = true;
|
|
3833
4072
|
}
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
4073
|
+
for (const room of ready) {
|
|
4074
|
+
if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
|
|
4075
|
+
if (attempt === 'authorized')
|
|
4076
|
+
name(room.locator);
|
|
4077
|
+
continue;
|
|
4078
|
+
}
|
|
4079
|
+
void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
|
|
4080
|
+
}
|
|
4081
|
+
const failed = prepared.find((room) => room.status === 'rejected');
|
|
3839
4082
|
if (failed !== undefined)
|
|
3840
4083
|
throw failed.reason;
|
|
3841
4084
|
}
|
|
@@ -3853,27 +4096,31 @@ export function composeRuntime(options) {
|
|
|
3853
4096
|
return false;
|
|
3854
4097
|
}
|
|
3855
4098
|
if (prior !== undefined)
|
|
3856
|
-
await retainModel(local, room,
|
|
4099
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
4100
|
+
if (prior !== undefined)
|
|
4101
|
+
await keepNaming(local, room);
|
|
3857
4102
|
if (discoveryPaused || authorization === undefined)
|
|
3858
4103
|
return false;
|
|
3859
4104
|
return room.replica.admission === 'admitted';
|
|
3860
4105
|
}
|
|
3861
4106
|
async function caughtUp(room, local, progress, live) {
|
|
3862
|
-
if (
|
|
4107
|
+
if (options.adapter.roomStream !== undefined || authorization === undefined)
|
|
4108
|
+
return;
|
|
4109
|
+
if (!live() || !behind(room))
|
|
3863
4110
|
return;
|
|
3864
4111
|
const withheld = await advance(room, local, authorization, live);
|
|
3865
4112
|
if (withheld !== null)
|
|
3866
4113
|
await withhold(withheld, room, local, progress);
|
|
3867
4114
|
}
|
|
3868
4115
|
function revoked(locator) {
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3872
|
-
});
|
|
4116
|
+
const model = modelOf(locator);
|
|
4117
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3873
4118
|
}
|
|
3874
4119
|
async function loadRoom(room, local, progress, live) {
|
|
3875
4120
|
if (!live() || authorization === undefined)
|
|
3876
4121
|
return;
|
|
4122
|
+
if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
|
|
4123
|
+
return;
|
|
3877
4124
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
3878
4125
|
await reload(room, local, progress, live);
|
|
3879
4126
|
return;
|
|
@@ -3891,21 +4138,53 @@ export function composeRuntime(options) {
|
|
|
3891
4138
|
reloadsDue.delete(room.locator);
|
|
3892
4139
|
if (!live() || authorization === undefined)
|
|
3893
4140
|
return;
|
|
4141
|
+
const from = standingOf(room.locator);
|
|
3894
4142
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
3895
|
-
if (!live())
|
|
3896
|
-
return;
|
|
3897
4143
|
if (bootstrapped.kind !== 'staged') {
|
|
3898
|
-
|
|
4144
|
+
if (live())
|
|
4145
|
+
await withhold(bootstrapped, room, local, progress);
|
|
3899
4146
|
return;
|
|
3900
4147
|
}
|
|
3901
|
-
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
4148
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
|
|
3902
4149
|
await caughtUp(room, local, progress, live);
|
|
3903
4150
|
}
|
|
3904
4151
|
finally {
|
|
3905
4152
|
caughtUpTo(room.locator);
|
|
3906
4153
|
}
|
|
3907
4154
|
}
|
|
3908
|
-
async function
|
|
4155
|
+
async function asked(authorized) {
|
|
4156
|
+
try {
|
|
4157
|
+
return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
|
|
4158
|
+
}
|
|
4159
|
+
catch {
|
|
4160
|
+
return { kind: 'transport-uncertainty' };
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
function malformed(listed) {
|
|
4164
|
+
for (const room of listed) {
|
|
4165
|
+
if (room.operations === undefined ||
|
|
4166
|
+
!['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
4167
|
+
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
4168
|
+
room.replica === undefined ||
|
|
4169
|
+
typeof room.replica.selection !== 'string' ||
|
|
4170
|
+
room.replica.selection.length === 0 ||
|
|
4171
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
4172
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
4173
|
+
return `invalid model or replica declaration ${room.locator}`;
|
|
4174
|
+
}
|
|
4175
|
+
if (!declaredSelection(room))
|
|
4176
|
+
return `invalid typed selection declaration ${room.locator}`;
|
|
4177
|
+
}
|
|
4178
|
+
return null;
|
|
4179
|
+
}
|
|
4180
|
+
async function signIn(local, signedIn) {
|
|
4181
|
+
if (JSON.stringify(signedIn) === JSON.stringify(member))
|
|
4182
|
+
return;
|
|
4183
|
+
await local.fact('member', signedIn);
|
|
4184
|
+
member = signedIn;
|
|
4185
|
+
emitMember();
|
|
4186
|
+
}
|
|
4187
|
+
async function discoverRooms(local, progress, attempt, live, found) {
|
|
3909
4188
|
if (closed || authorization === undefined) {
|
|
3910
4189
|
notifyReady();
|
|
3911
4190
|
return;
|
|
@@ -3913,13 +4192,9 @@ export function composeRuntime(options) {
|
|
|
3913
4192
|
moveTo(transferring(status));
|
|
3914
4193
|
discoveries += 1;
|
|
3915
4194
|
const started = discoveries;
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
}
|
|
3920
|
-
catch {
|
|
3921
|
-
result = { kind: 'transport-uncertainty' };
|
|
3922
|
-
}
|
|
4195
|
+
const result = found ?? (await asked(authorization));
|
|
4196
|
+
if (result.kind === 'discovery')
|
|
4197
|
+
roomsNamed = true;
|
|
3923
4198
|
if (!live()) {
|
|
3924
4199
|
progress.complete = false;
|
|
3925
4200
|
notifyReady();
|
|
@@ -3927,12 +4202,7 @@ export function composeRuntime(options) {
|
|
|
3927
4202
|
}
|
|
3928
4203
|
journal.record(`connection discover ${result.kind}`);
|
|
3929
4204
|
if (result.kind === 'transport-uncertainty') {
|
|
3930
|
-
|
|
3931
|
-
await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
|
|
3932
|
-
if (room.typedSelection !== null) {
|
|
3933
|
-
await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
|
|
3934
|
-
}
|
|
3935
|
-
}
|
|
4205
|
+
await cachedModels(local);
|
|
3936
4206
|
progress.reachable = false;
|
|
3937
4207
|
progress.complete = false;
|
|
3938
4208
|
notifyReady();
|
|
@@ -3953,26 +4223,12 @@ export function composeRuntime(options) {
|
|
|
3953
4223
|
notifyReady();
|
|
3954
4224
|
return;
|
|
3955
4225
|
}
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
room.replica.selection.length === 0 ||
|
|
3963
|
-
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
3964
|
-
!['locked', 'open'].includes(room.replica.offline)) {
|
|
3965
|
-
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
3966
|
-
await refuse('missing-required-guarantee');
|
|
3967
|
-
notifyReady();
|
|
3968
|
-
return;
|
|
3969
|
-
}
|
|
3970
|
-
if (!declaredSelection(room)) {
|
|
3971
|
-
journal.record(`invalid typed selection declaration ${room.locator}`);
|
|
3972
|
-
await refuse('missing-required-guarantee');
|
|
3973
|
-
notifyReady();
|
|
3974
|
-
return;
|
|
3975
|
-
}
|
|
4226
|
+
const invalid = malformed(result.rooms);
|
|
4227
|
+
if (invalid !== null) {
|
|
4228
|
+
journal.record(invalid);
|
|
4229
|
+
await refuse('missing-required-guarantee');
|
|
4230
|
+
notifyReady();
|
|
4231
|
+
return;
|
|
3976
4232
|
}
|
|
3977
4233
|
if (refusal !== null) {
|
|
3978
4234
|
refusal = null;
|
|
@@ -3988,13 +4244,8 @@ export function composeRuntime(options) {
|
|
|
3988
4244
|
await local.fact('providerLimits', limits);
|
|
3989
4245
|
await local.fact('capabilities', capabilities);
|
|
3990
4246
|
}
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
await local.fact('member', signedIn);
|
|
3994
|
-
member = signedIn;
|
|
3995
|
-
emitMember();
|
|
3996
|
-
}
|
|
3997
|
-
await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
|
|
4247
|
+
await signIn(local, result.member ?? null);
|
|
4248
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
3998
4249
|
}
|
|
3999
4250
|
async function ensureAuthorization() {
|
|
4000
4251
|
if (authorization !== undefined)
|
|
@@ -4009,23 +4260,45 @@ export function composeRuntime(options) {
|
|
|
4009
4260
|
}
|
|
4010
4261
|
}
|
|
4011
4262
|
async function admittedRead(work) {
|
|
4012
|
-
|
|
4013
|
-
|
|
4263
|
+
while (roomReads.free === 0) {
|
|
4264
|
+
await new Promise((admit) => roomReads.waiting.push(admit));
|
|
4014
4265
|
}
|
|
4015
|
-
|
|
4016
|
-
while (reads.free === 0)
|
|
4017
|
-
await new Promise((admit) => reads.waiting.push(admit));
|
|
4018
|
-
reads.free -= 1;
|
|
4266
|
+
roomReads.free -= 1;
|
|
4019
4267
|
try {
|
|
4020
4268
|
await work();
|
|
4021
4269
|
}
|
|
4022
4270
|
finally {
|
|
4023
|
-
|
|
4024
|
-
|
|
4271
|
+
roomReads.free += 1;
|
|
4272
|
+
roomReads.waiting.shift()?.();
|
|
4025
4273
|
}
|
|
4026
4274
|
}
|
|
4275
|
+
function inRoom(locator, work) {
|
|
4276
|
+
roomWork += 1;
|
|
4277
|
+
const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
|
|
4278
|
+
try {
|
|
4279
|
+
return await work(continuing);
|
|
4280
|
+
}
|
|
4281
|
+
finally {
|
|
4282
|
+
roomWork -= 1;
|
|
4283
|
+
}
|
|
4284
|
+
});
|
|
4285
|
+
const chain = ran.then(() => undefined, () => undefined);
|
|
4286
|
+
roomLanes.set(locator, chain);
|
|
4287
|
+
working.add(chain);
|
|
4288
|
+
void chain.finally(() => {
|
|
4289
|
+
working.delete(chain);
|
|
4290
|
+
if (roomLanes.get(locator) === chain)
|
|
4291
|
+
roomLanes.delete(locator);
|
|
4292
|
+
});
|
|
4293
|
+
return ran;
|
|
4294
|
+
}
|
|
4027
4295
|
function roomTask(locator, work) {
|
|
4028
|
-
|
|
4296
|
+
if (stopping)
|
|
4297
|
+
return Promise.resolve();
|
|
4298
|
+
return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
|
|
4299
|
+
}
|
|
4300
|
+
function advanced(room, local, authorized) {
|
|
4301
|
+
return inRoom(room.locator, (live) => advance(room, local, authorized, live));
|
|
4029
4302
|
}
|
|
4030
4303
|
function task(lane, work) {
|
|
4031
4304
|
if (stopping)
|
|
@@ -4041,23 +4314,19 @@ export function composeRuntime(options) {
|
|
|
4041
4314
|
void run.finally(() => working.delete(run));
|
|
4042
4315
|
return run;
|
|
4043
4316
|
}
|
|
4044
|
-
function sync(attempt) {
|
|
4317
|
+
function sync(attempt, found) {
|
|
4045
4318
|
return task(OPENING_LANE, async (live) => {
|
|
4046
4319
|
if (!live()) {
|
|
4047
|
-
|
|
4320
|
+
readyFromStore();
|
|
4048
4321
|
return;
|
|
4049
4322
|
}
|
|
4050
4323
|
const progress = { complete: true, reachable: true };
|
|
4051
|
-
|
|
4052
|
-
if (attempt === 'opened')
|
|
4053
|
-
opened = generation;
|
|
4054
|
-
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
4324
|
+
underway = attempt === 'opened' ? progress : null;
|
|
4055
4325
|
if (attempt === 'opened')
|
|
4056
4326
|
streamed = progress;
|
|
4057
4327
|
complete = false;
|
|
4058
4328
|
recorded = false;
|
|
4059
4329
|
moveTo(transferring(status));
|
|
4060
|
-
handed = false;
|
|
4061
4330
|
try {
|
|
4062
4331
|
const local = await openLocal();
|
|
4063
4332
|
if (!live())
|
|
@@ -4065,7 +4334,7 @@ export function composeRuntime(options) {
|
|
|
4065
4334
|
await ensureAuthorization();
|
|
4066
4335
|
if (!live())
|
|
4067
4336
|
return;
|
|
4068
|
-
await discoverRooms(local, progress, attempt, live);
|
|
4337
|
+
await discoverRooms(local, progress, attempt, live, found);
|
|
4069
4338
|
}
|
|
4070
4339
|
catch {
|
|
4071
4340
|
progress.complete = false;
|
|
@@ -4073,7 +4342,6 @@ export function composeRuntime(options) {
|
|
|
4073
4342
|
progress.reachable = false;
|
|
4074
4343
|
}
|
|
4075
4344
|
finally {
|
|
4076
|
-
gate.release();
|
|
4077
4345
|
notifyReady();
|
|
4078
4346
|
}
|
|
4079
4347
|
if (!live())
|
|
@@ -4085,35 +4353,59 @@ export function composeRuntime(options) {
|
|
|
4085
4353
|
if (handed || closed || progress.reachable)
|
|
4086
4354
|
return;
|
|
4087
4355
|
underway = null;
|
|
4088
|
-
opened = null;
|
|
4089
|
-
journal.record('connection reopen');
|
|
4090
4356
|
await rest(progress, live);
|
|
4091
|
-
options.adapter.roomStream?.reopen();
|
|
4092
4357
|
});
|
|
4093
4358
|
}
|
|
4094
|
-
function
|
|
4359
|
+
function resynchronize() {
|
|
4095
4360
|
return task(OPENING_LANE, async (live) => {
|
|
4096
|
-
if (!live() || !
|
|
4361
|
+
if (!live() || !announced)
|
|
4097
4362
|
return;
|
|
4098
4363
|
const progress = { complete: true, reachable: true };
|
|
4099
|
-
const gate = discover();
|
|
4100
4364
|
moveTo(transferring(status));
|
|
4101
4365
|
try {
|
|
4102
4366
|
const local = await openLocal();
|
|
4103
4367
|
if (!live())
|
|
4104
4368
|
return;
|
|
4105
|
-
await
|
|
4369
|
+
await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
|
|
4370
|
+
}
|
|
4371
|
+
catch {
|
|
4372
|
+
progress.complete = false;
|
|
4373
|
+
}
|
|
4374
|
+
if (live())
|
|
4375
|
+
await finishOpening(progress, live);
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
4378
|
+
function countAuthorityChange() {
|
|
4379
|
+
authorityChanges += 1;
|
|
4380
|
+
for (const listener of authorityChangeListeners)
|
|
4381
|
+
listener(authorityChanges);
|
|
4382
|
+
}
|
|
4383
|
+
function changeAuthority(change) {
|
|
4384
|
+
return task(OPENING_LANE, async (live) => {
|
|
4385
|
+
if (!live() || !announced)
|
|
4386
|
+
return;
|
|
4387
|
+
countAuthorityChange();
|
|
4388
|
+
const progress = { complete: true, reachable: true };
|
|
4389
|
+
moveTo(transferring(status));
|
|
4390
|
+
try {
|
|
4391
|
+
const local = await openLocal();
|
|
4106
4392
|
if (!live())
|
|
4107
4393
|
return;
|
|
4108
|
-
|
|
4394
|
+
const invalid = malformed(change.rooms);
|
|
4395
|
+
if (invalid !== null) {
|
|
4396
|
+
journal.record(invalid);
|
|
4397
|
+
await refuse('missing-required-guarantee');
|
|
4398
|
+
return;
|
|
4399
|
+
}
|
|
4400
|
+
if (change.member !== undefined)
|
|
4401
|
+
await signIn(local, change.member);
|
|
4402
|
+
discoveries += 1;
|
|
4403
|
+
const listed = await applyDiscovery(change.rooms, discoveries, change.left);
|
|
4404
|
+
await synchronize(listed, local, progress, 'authorized', live);
|
|
4109
4405
|
}
|
|
4110
4406
|
catch {
|
|
4111
4407
|
progress.complete = false;
|
|
4112
4408
|
}
|
|
4113
|
-
finally {
|
|
4114
|
-
gate.release();
|
|
4115
|
-
notifyReady();
|
|
4116
|
-
}
|
|
4117
4409
|
if (!live())
|
|
4118
4410
|
return;
|
|
4119
4411
|
if (!progress.complete)
|
|
@@ -4121,14 +4413,17 @@ export function composeRuntime(options) {
|
|
|
4121
4413
|
await rest(progress, live);
|
|
4122
4414
|
});
|
|
4123
4415
|
}
|
|
4124
|
-
function closedSocket(revoked, lost) {
|
|
4125
|
-
|
|
4416
|
+
function closedSocket(revoked, lost, authorityChanged) {
|
|
4417
|
+
notifyReady();
|
|
4418
|
+
const opened = announced;
|
|
4126
4419
|
socketOpen = false;
|
|
4420
|
+
announced = false;
|
|
4421
|
+
missedChange ||= authorityChanged;
|
|
4422
|
+
handed = false;
|
|
4423
|
+
naming = 0;
|
|
4127
4424
|
connectionLost();
|
|
4128
|
-
if (lost)
|
|
4129
|
-
reads.lost();
|
|
4425
|
+
if (lost)
|
|
4130
4426
|
conversations.lost();
|
|
4131
|
-
}
|
|
4132
4427
|
if (revoked) {
|
|
4133
4428
|
void refuse('revoked-authorization');
|
|
4134
4429
|
return;
|
|
@@ -4139,18 +4434,32 @@ export function composeRuntime(options) {
|
|
|
4139
4434
|
moveTo({
|
|
4140
4435
|
...status,
|
|
4141
4436
|
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
4142
|
-
providerUnreachable: status.providerUnreachable || !opened,
|
|
4437
|
+
providerUnreachable: status.providerUnreachable || (!opened && lost),
|
|
4438
|
+
});
|
|
4439
|
+
void closing(opened);
|
|
4440
|
+
}
|
|
4441
|
+
function closing(opened) {
|
|
4442
|
+
return task(OPENING_LANE, async (live) => {
|
|
4443
|
+
if (!live())
|
|
4444
|
+
return;
|
|
4445
|
+
const local = await openLocal();
|
|
4446
|
+
if (!opened && !socketOpen)
|
|
4447
|
+
await cachedModels(local);
|
|
4448
|
+
await rest(null, live);
|
|
4143
4449
|
});
|
|
4144
4450
|
}
|
|
4145
|
-
function
|
|
4146
|
-
|
|
4451
|
+
function cachedModels(local) {
|
|
4452
|
+
return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
|
|
4453
|
+
}
|
|
4454
|
+
function caughtUpRooms(expired) {
|
|
4455
|
+
const progress = underway;
|
|
4456
|
+
underway = null;
|
|
4457
|
+
naming = Math.max(0, naming - 1);
|
|
4147
4458
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
4148
|
-
|
|
4149
|
-
opened = null;
|
|
4459
|
+
replaced(expired);
|
|
4150
4460
|
const reloading = progress === null ? [] : owedReloads(expired);
|
|
4151
4461
|
const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
|
|
4152
4462
|
return task(OPENING_LANE, async (live) => {
|
|
4153
|
-
underway = null;
|
|
4154
4463
|
if (!live())
|
|
4155
4464
|
return;
|
|
4156
4465
|
if (progress === null) {
|
|
@@ -4158,24 +4467,14 @@ export function composeRuntime(options) {
|
|
|
4158
4467
|
await rest(cycle, live);
|
|
4159
4468
|
return;
|
|
4160
4469
|
}
|
|
4161
|
-
await roomsCarried([...expired, ...reloading]);
|
|
4162
|
-
if (!live())
|
|
4163
|
-
return;
|
|
4164
4470
|
try {
|
|
4165
4471
|
const local = await openLocal();
|
|
4166
|
-
|
|
4472
|
+
await replacing;
|
|
4167
4473
|
if (progress.reachable) {
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
!reloading.includes(room.locator)) {
|
|
4172
|
-
await confirmModels(room, local);
|
|
4173
|
-
}
|
|
4174
|
-
}
|
|
4474
|
+
await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
|
|
4475
|
+
!expired.includes(room.locator) &&
|
|
4476
|
+
!reloading.includes(room.locator)), 'current', local);
|
|
4175
4477
|
}
|
|
4176
|
-
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
4177
|
-
if (failed !== undefined)
|
|
4178
|
-
throw failed.reason;
|
|
4179
4478
|
}
|
|
4180
4479
|
catch {
|
|
4181
4480
|
progress.complete = false;
|
|
@@ -4187,11 +4486,8 @@ export function composeRuntime(options) {
|
|
|
4187
4486
|
}
|
|
4188
4487
|
function owedReloads(expired) {
|
|
4189
4488
|
const reloading = [...discovered.values()]
|
|
4190
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
4191
|
-
!expired.includes(room.locator) &&
|
|
4192
|
-
(owed.has(room.locator) || revoked(room.locator)))
|
|
4489
|
+
.filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
|
|
4193
4490
|
.map((room) => room.locator);
|
|
4194
|
-
owed.clear();
|
|
4195
4491
|
for (const locator of reloading)
|
|
4196
4492
|
catching.add(locator);
|
|
4197
4493
|
liveQueries.recover();
|
|
@@ -4200,24 +4496,43 @@ export function composeRuntime(options) {
|
|
|
4200
4496
|
async function reloadNow(replaced, progress) {
|
|
4201
4497
|
const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
|
|
4202
4498
|
for (const room of reloaded)
|
|
4203
|
-
reloadsDue.
|
|
4499
|
+
reloadsDue.add(room.locator);
|
|
4204
4500
|
try {
|
|
4205
4501
|
const local = await openLocal();
|
|
4206
|
-
|
|
4502
|
+
for (const room of reloaded) {
|
|
4503
|
+
void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
|
|
4504
|
+
}
|
|
4207
4505
|
}
|
|
4208
4506
|
catch {
|
|
4209
4507
|
for (const room of reloaded)
|
|
4210
4508
|
reloadsDue.delete(room.locator);
|
|
4211
4509
|
progress.reachable = false;
|
|
4212
|
-
return [];
|
|
4213
4510
|
}
|
|
4214
4511
|
}
|
|
4215
|
-
async function
|
|
4216
|
-
|
|
4217
|
-
const model = modelOf(room.locator
|
|
4218
|
-
|
|
4219
|
-
|
|
4512
|
+
async function settleModels(held, status, local) {
|
|
4513
|
+
const owed = (room) => {
|
|
4514
|
+
const model = modelOf(room.locator);
|
|
4515
|
+
return model.status !== 'unavailable' && model.status !== status;
|
|
4516
|
+
};
|
|
4517
|
+
const settled = await exclusive(async () => {
|
|
4518
|
+
if (!continuing())
|
|
4519
|
+
return [];
|
|
4520
|
+
const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
|
|
4521
|
+
for (const room of unchanged) {
|
|
4522
|
+
const next = { ...modelOf(room.locator), status };
|
|
4523
|
+
const replica = replicas.get(room.locator);
|
|
4524
|
+
if (replica !== undefined)
|
|
4525
|
+
replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
|
|
4526
|
+
heldModel(room, next);
|
|
4220
4527
|
}
|
|
4528
|
+
if (unchanged.length > 0)
|
|
4529
|
+
emitRooms();
|
|
4530
|
+
return unchanged;
|
|
4531
|
+
});
|
|
4532
|
+
for (const room of held) {
|
|
4533
|
+
if (settled.includes(room) || !owed(room))
|
|
4534
|
+
continue;
|
|
4535
|
+
await retainModel(local, room, { ...modelOf(room.locator), status });
|
|
4221
4536
|
}
|
|
4222
4537
|
}
|
|
4223
4538
|
function submitLater(locator) {
|
|
@@ -4262,14 +4577,10 @@ export function composeRuntime(options) {
|
|
|
4262
4577
|
try {
|
|
4263
4578
|
const local = await openLocal();
|
|
4264
4579
|
await roomTask(locator, async (live) => {
|
|
4265
|
-
|
|
4266
|
-
if (!current() || authorization === undefined)
|
|
4580
|
+
if (!live() || authorization === undefined)
|
|
4267
4581
|
return;
|
|
4268
|
-
const withheld = await
|
|
4269
|
-
|
|
4270
|
-
abandonedOn(signal),
|
|
4271
|
-
]);
|
|
4272
|
-
if (withheld !== null && current())
|
|
4582
|
+
const withheld = await advance(room, local, authorization, live);
|
|
4583
|
+
if (withheld !== null && live())
|
|
4273
4584
|
await withhold(withheld, room, local, progress);
|
|
4274
4585
|
});
|
|
4275
4586
|
}
|
|
@@ -4324,14 +4635,11 @@ export function composeRuntime(options) {
|
|
|
4324
4635
|
progress.complete = false;
|
|
4325
4636
|
return;
|
|
4326
4637
|
}
|
|
4327
|
-
reads.applied();
|
|
4328
4638
|
submitAll();
|
|
4329
4639
|
for (const locator of stale)
|
|
4330
4640
|
if (repairable.has(locator))
|
|
4331
4641
|
repairLater(locator, {});
|
|
4332
4642
|
await conversations.reconcile();
|
|
4333
|
-
if (status.providerUnreachable)
|
|
4334
|
-
conversations.reconnected();
|
|
4335
4643
|
const reading = { complete: true, reachable: true };
|
|
4336
4644
|
await conversations.catchUp(reading);
|
|
4337
4645
|
if (progress.reachable && reading.reachable)
|
|
@@ -4357,7 +4665,7 @@ export function composeRuntime(options) {
|
|
|
4357
4665
|
if (!live())
|
|
4358
4666
|
return;
|
|
4359
4667
|
let lastCompleted = status.lastCompleted;
|
|
4360
|
-
let synced = complete && stale.size === 0 && !pending();
|
|
4668
|
+
let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
|
|
4361
4669
|
if (synced && !recorded) {
|
|
4362
4670
|
const completedAt = new Date().toISOString();
|
|
4363
4671
|
try {
|
|
@@ -4372,74 +4680,78 @@ export function composeRuntime(options) {
|
|
|
4372
4680
|
}
|
|
4373
4681
|
}
|
|
4374
4682
|
moveTo({
|
|
4375
|
-
state: pending()
|
|
4683
|
+
state: pending()
|
|
4684
|
+
? 'pending'
|
|
4685
|
+
: synced && complete && roomWork === 0 && naming === 0
|
|
4686
|
+
? 'synced'
|
|
4687
|
+
: 'syncing',
|
|
4376
4688
|
lastCompleted,
|
|
4377
|
-
providerUnreachable: !progress.reachable,
|
|
4689
|
+
providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
|
|
4378
4690
|
...attention(),
|
|
4379
4691
|
});
|
|
4380
4692
|
}
|
|
4381
|
-
function
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
}
|
|
4389
|
-
function applyRoom(locator, roomVersion) {
|
|
4390
|
-
announced.set(locator, roomVersion);
|
|
4391
|
-
return task(roomLane(locator), async (live) => {
|
|
4392
|
-
if (discovering?.generation === generation)
|
|
4393
|
-
await discovering.done;
|
|
4394
|
-
const saving = answering.get(locator);
|
|
4395
|
-
if (saving?.generation === generation)
|
|
4396
|
-
await saving.answered;
|
|
4397
|
-
if (!live() || authorization === undefined)
|
|
4398
|
-
return;
|
|
4399
|
-
const room = discovered.get(locator);
|
|
4400
|
-
const held = replicas.get(locator);
|
|
4401
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
4402
|
-
return;
|
|
4403
|
-
if (held !== undefined &&
|
|
4404
|
-
(held.record.replica.selection !== room.replica.selection ||
|
|
4405
|
-
options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
|
|
4406
|
-
return;
|
|
4407
|
-
}
|
|
4408
|
-
const during = underway?.generation === generation ? underway.progress : null;
|
|
4409
|
-
const progress = during ?? { complete: true, reachable: true };
|
|
4410
|
-
if (during === null)
|
|
4411
|
-
moveTo(transferring(status));
|
|
4693
|
+
function applyRoom(change) {
|
|
4694
|
+
const { locator } = change;
|
|
4695
|
+
if (stopping)
|
|
4696
|
+
return Promise.resolve();
|
|
4697
|
+
return inRoom(locator, async (live) => {
|
|
4698
|
+
await answering.get(locator)?.answered;
|
|
4699
|
+
const progress = underway ?? { complete: true, reachable: true };
|
|
4412
4700
|
try {
|
|
4413
4701
|
const local = await openLocal();
|
|
4414
|
-
|
|
4415
|
-
if (!live()
|
|
4416
|
-
return;
|
|
4417
|
-
const
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4702
|
+
for (;;) {
|
|
4703
|
+
if (!live())
|
|
4704
|
+
return null;
|
|
4705
|
+
const room = discovered.get(locator);
|
|
4706
|
+
const held = replicas.get(locator);
|
|
4707
|
+
if (room === undefined ||
|
|
4708
|
+
held === undefined ||
|
|
4709
|
+
room.replica.admission !== 'admitted' ||
|
|
4710
|
+
held.record.replica.selection !== room.replica.selection) {
|
|
4711
|
+
return null;
|
|
4712
|
+
}
|
|
4713
|
+
const at = held.record.roomVersion;
|
|
4714
|
+
if (options.adapter.reached(locator, at, change.roomVersion))
|
|
4715
|
+
return progress;
|
|
4716
|
+
if (!options.adapter.reached(locator, at, change.from)) {
|
|
4717
|
+
name(locator);
|
|
4718
|
+
return progress;
|
|
4719
|
+
}
|
|
4720
|
+
if (underway === null)
|
|
4721
|
+
moveTo(transferring(status));
|
|
4722
|
+
const writing = () => live() && !full;
|
|
4723
|
+
let outlined = false;
|
|
4724
|
+
const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, async () => {
|
|
4725
|
+
const page = await change.read();
|
|
4726
|
+
const paths = Object.keys(page.outlines ?? {});
|
|
4727
|
+
outlined = paths.length > 0;
|
|
4728
|
+
await local.remember(locator, held.record.key, paths);
|
|
4729
|
+
return page;
|
|
4730
|
+
}));
|
|
4731
|
+
if (refreshed.kind !== 'advanced')
|
|
4732
|
+
return progress;
|
|
4733
|
+
const landing = {
|
|
4734
|
+
kind: 'refreshed',
|
|
4735
|
+
from: refreshed.from,
|
|
4736
|
+
remodelled: refreshed.remodelled,
|
|
4737
|
+
};
|
|
4738
|
+
if (await publish(local, room, refreshed.record, landing, writing)) {
|
|
4739
|
+
stale.delete(locator);
|
|
4740
|
+
await retainModel(local, room, modelOf(locator));
|
|
4741
|
+
if (outlined)
|
|
4742
|
+
void heldAgain(locator);
|
|
4743
|
+
return progress;
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4421
4746
|
}
|
|
4422
4747
|
catch {
|
|
4748
|
+
if (!stale.has(locator))
|
|
4749
|
+
name(locator);
|
|
4423
4750
|
stale.add(locator);
|
|
4751
|
+
return progress;
|
|
4424
4752
|
}
|
|
4425
|
-
|
|
4426
|
-
if (during === null)
|
|
4427
|
-
await rest(progress, live);
|
|
4428
|
-
}
|
|
4429
|
-
});
|
|
4753
|
+
}).then((progress) => rest(progress), () => undefined);
|
|
4430
4754
|
}
|
|
4431
|
-
function roomsCarried(replacing) {
|
|
4432
|
-
const replaced = new Set(replacing.map(roomLane));
|
|
4433
|
-
return Promise.all([...lanes]
|
|
4434
|
-
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
4435
|
-
.map(([, held]) => held.chain));
|
|
4436
|
-
}
|
|
4437
|
-
const receipts = {
|
|
4438
|
-
heads: async () => (await openLocal()).receivedHeads(),
|
|
4439
|
-
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
4440
|
-
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
4441
|
-
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
4442
|
-
};
|
|
4443
4755
|
function observeStream() {
|
|
4444
4756
|
const stream = options.adapter.roomStream;
|
|
4445
4757
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -4449,29 +4761,35 @@ export function composeRuntime(options) {
|
|
|
4449
4761
|
return;
|
|
4450
4762
|
if (event.kind === 'opened') {
|
|
4451
4763
|
socketOpen = true;
|
|
4764
|
+
announced = false;
|
|
4765
|
+
handed = false;
|
|
4766
|
+
naming = 0;
|
|
4452
4767
|
connectionLive();
|
|
4453
4768
|
generation += 1;
|
|
4454
|
-
reads.opened();
|
|
4455
4769
|
conversations.reopened();
|
|
4456
4770
|
recovery.resume();
|
|
4457
|
-
|
|
4771
|
+
}
|
|
4772
|
+
else if (event.kind === 'discovered') {
|
|
4773
|
+
if (!socketOpen)
|
|
4774
|
+
return;
|
|
4775
|
+
announced = true;
|
|
4776
|
+
if (missedChange) {
|
|
4777
|
+
missedChange = false;
|
|
4778
|
+
countAuthorityChange();
|
|
4779
|
+
}
|
|
4780
|
+
void sync('opened', event.discovery);
|
|
4781
|
+
for (const locator of replicas.keys())
|
|
4782
|
+
void heldAgain(locator);
|
|
4458
4783
|
}
|
|
4459
4784
|
else if (event.kind === 'closed')
|
|
4460
|
-
closedSocket(event.revoked, event.lost);
|
|
4461
|
-
else if (event.kind === '
|
|
4462
|
-
void
|
|
4785
|
+
closedSocket(event.revoked, event.lost, event.authorityChanged);
|
|
4786
|
+
else if (event.kind === 'caught-up')
|
|
4787
|
+
void caughtUpRooms(event.expired);
|
|
4463
4788
|
else if (event.kind === 'authority-changed')
|
|
4464
|
-
void
|
|
4789
|
+
void changeAuthority(event);
|
|
4465
4790
|
else
|
|
4466
|
-
void applyRoom(event
|
|
4467
|
-
}
|
|
4468
|
-
}
|
|
4469
|
-
async function heldRevisions(locator, paths) {
|
|
4470
|
-
const held = replicas.get(locator);
|
|
4471
|
-
if (held === undefined || paths.length === 0)
|
|
4472
|
-
return new Map();
|
|
4473
|
-
const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
|
|
4474
|
-
return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
|
|
4791
|
+
void applyRoom(event);
|
|
4792
|
+
});
|
|
4475
4793
|
}
|
|
4476
4794
|
function writesHeld() {
|
|
4477
4795
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -4541,8 +4859,9 @@ export function composeRuntime(options) {
|
|
|
4541
4859
|
if (closed)
|
|
4542
4860
|
return;
|
|
4543
4861
|
closed = true;
|
|
4544
|
-
|
|
4545
|
-
|
|
4862
|
+
notifyDiscovered();
|
|
4863
|
+
if (probing !== null)
|
|
4864
|
+
clearInterval(probing);
|
|
4546
4865
|
connectionLive();
|
|
4547
4866
|
lockListeners.clear();
|
|
4548
4867
|
readinessWatches.clear();
|
|
@@ -4553,16 +4872,14 @@ export function composeRuntime(options) {
|
|
|
4553
4872
|
withdrawServiceMode?.();
|
|
4554
4873
|
conversations.close();
|
|
4555
4874
|
void recovery.retire();
|
|
4556
|
-
store?.close();
|
|
4557
|
-
void opening?.then((opened) => {
|
|
4558
|
-
opened.close();
|
|
4559
|
-
}, () => { });
|
|
4560
4875
|
roomListeners.clear();
|
|
4876
|
+
unlistedRooms.clear();
|
|
4561
4877
|
refusalListeners.clear();
|
|
4562
4878
|
storageLossListeners.clear();
|
|
4563
4879
|
statusListeners.clear();
|
|
4564
4880
|
serviceModeListeners.clear();
|
|
4565
4881
|
memberListeners.clear();
|
|
4882
|
+
authorityChangeListeners.clear();
|
|
4566
4883
|
liveQueries.close();
|
|
4567
4884
|
queueSubscriptions.clear();
|
|
4568
4885
|
roomDraftListeners.clear();
|
|
@@ -4570,6 +4887,7 @@ export function composeRuntime(options) {
|
|
|
4570
4887
|
}
|
|
4571
4888
|
async function stop() {
|
|
4572
4889
|
stopping = true;
|
|
4890
|
+
inFlight.abort();
|
|
4573
4891
|
await recovery.retire();
|
|
4574
4892
|
await Promise.all(working);
|
|
4575
4893
|
await mutations;
|
|
@@ -4583,12 +4901,12 @@ export function composeRuntime(options) {
|
|
|
4583
4901
|
observeStream();
|
|
4584
4902
|
void openLocal().then(() => {
|
|
4585
4903
|
if (capabilities.length > 0)
|
|
4586
|
-
|
|
4904
|
+
readyFromStore();
|
|
4587
4905
|
}, () => {
|
|
4588
|
-
|
|
4906
|
+
readyFromStore();
|
|
4589
4907
|
});
|
|
4590
4908
|
if (options.adapter.roomStream === undefined)
|
|
4591
|
-
void sync('unstreamed');
|
|
4909
|
+
void sync('unstreamed', null);
|
|
4592
4910
|
function planned(input, reach) {
|
|
4593
4911
|
const plan = planOf(input, roomFacts(reach), planReach(reach));
|
|
4594
4912
|
const named = plan.paths !== null &&
|
|
@@ -4596,7 +4914,7 @@ export function composeRuntime(options) {
|
|
|
4596
4914
|
plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
|
|
4597
4915
|
return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
|
|
4598
4916
|
}
|
|
4599
|
-
function watchPlan(plan, deliver) {
|
|
4917
|
+
function watchPlan(plan, deliver, fail) {
|
|
4600
4918
|
let seeded = false;
|
|
4601
4919
|
let pending = true;
|
|
4602
4920
|
const shown = () => {
|
|
@@ -4622,7 +4940,7 @@ export function composeRuntime(options) {
|
|
|
4622
4940
|
}
|
|
4623
4941
|
seeded = false;
|
|
4624
4942
|
deliver(message);
|
|
4625
|
-
});
|
|
4943
|
+
}, fail);
|
|
4626
4944
|
return () => {
|
|
4627
4945
|
watchers.delete(shown);
|
|
4628
4946
|
if (watchers.size === 0)
|
|
@@ -4630,86 +4948,59 @@ export function composeRuntime(options) {
|
|
|
4630
4948
|
stop();
|
|
4631
4949
|
};
|
|
4632
4950
|
}
|
|
4633
|
-
async function searched(input, reach
|
|
4951
|
+
async function searched(input, reach) {
|
|
4634
4952
|
const asked = searchPlanOf(input);
|
|
4635
|
-
|
|
4636
|
-
const
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
};
|
|
4679
|
-
const answered = covered(answering);
|
|
4680
|
-
if ('pending' in answered)
|
|
4681
|
-
continue;
|
|
4682
|
-
const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
|
|
4683
|
-
const seen = searchIndex.changes();
|
|
4684
|
-
await searchIndex.caughtUp();
|
|
4685
|
-
const selected = roomsAlone(answering) ? null : await everyRow(answering);
|
|
4686
|
-
if (selected !== null && 'pending' in selected)
|
|
4687
|
-
continue;
|
|
4688
|
-
if (searchIndex.changes() !== seen ||
|
|
4689
|
-
JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
4690
|
-
continue;
|
|
4691
|
-
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms !== null
|
|
4692
|
-
? new Set(answering.rooms)
|
|
4693
|
-
: locked.length === 0
|
|
4694
|
-
? null
|
|
4695
|
-
: new Set(reachedRooms(answering)), plan.collection
|
|
4696
|
-
? new Map(answered.coverage.rooms.map(({ room }) => [
|
|
4697
|
-
room,
|
|
4698
|
-
replicas.get(room)?.record.typedSelection?.roots ?? [],
|
|
4699
|
-
]))
|
|
4700
|
-
: null);
|
|
4701
|
-
const coverage = {
|
|
4702
|
-
rooms: answered.coverage.rooms,
|
|
4703
|
-
conversations: searchedConversations,
|
|
4704
|
-
index: searchIndex.generation(),
|
|
4705
|
-
overlay: answered.coverage.overlay,
|
|
4953
|
+
await ready;
|
|
4954
|
+
const search = options.adapter.search;
|
|
4955
|
+
if (search === undefined) {
|
|
4956
|
+
return {
|
|
4957
|
+
kind: 'undeclared',
|
|
4958
|
+
code: 'action-undeclared',
|
|
4959
|
+
fields: { name: 'search' },
|
|
4960
|
+
retryable: false,
|
|
4961
|
+
};
|
|
4962
|
+
}
|
|
4963
|
+
if (closed || refusal !== null || connectionOf() !== 'connected')
|
|
4964
|
+
return OFFLINE;
|
|
4965
|
+
const reached = reach === null ? null : new Set(reach.rooms);
|
|
4966
|
+
const named = asked.rooms === null
|
|
4967
|
+
? reached === null
|
|
4968
|
+
? null
|
|
4969
|
+
: [...reached]
|
|
4970
|
+
: asked.rooms.filter((room) => reached === null || reached.has(room));
|
|
4971
|
+
const rooms = named === null ? null : named.filter((room) => replicas.has(room)).sort();
|
|
4972
|
+
if (rooms !== null && rooms.length === 0)
|
|
4973
|
+
return { kind: 'found', documents: [] };
|
|
4974
|
+
try {
|
|
4975
|
+
await ensureAuthorization();
|
|
4976
|
+
}
|
|
4977
|
+
catch {
|
|
4978
|
+
return UNCERTAIN;
|
|
4979
|
+
}
|
|
4980
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
4981
|
+
return UNCERTAIN;
|
|
4982
|
+
journal.record('transfer search');
|
|
4983
|
+
try {
|
|
4984
|
+
const result = await search(authorization, { text: asked.text, rooms });
|
|
4985
|
+
if (result.kind === 'revoked-authorization')
|
|
4986
|
+
await refuse('revoked-authorization');
|
|
4987
|
+
journal.record(`outcome search ${result.kind}`);
|
|
4988
|
+
if (result.kind === 'rejected')
|
|
4989
|
+
return result;
|
|
4990
|
+
if (result.kind !== 'found')
|
|
4991
|
+
return failure(result);
|
|
4992
|
+
const kept = rooms === null ? null : new Set(rooms);
|
|
4993
|
+
return {
|
|
4994
|
+
kind: 'found',
|
|
4995
|
+
documents: result.documents.filter((hit) => kept === null || kept.has(hit.room)),
|
|
4706
4996
|
};
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4997
|
+
}
|
|
4998
|
+
catch {
|
|
4999
|
+
journal.record('outcome search transport-uncertainty');
|
|
5000
|
+
return UNCERTAIN;
|
|
4710
5001
|
}
|
|
4711
5002
|
}
|
|
4712
|
-
const watch = (topic, input, deliver) => {
|
|
5003
|
+
const watch = (topic, input, deliver, fail) => {
|
|
4713
5004
|
if (topic === 'room-drafts') {
|
|
4714
5005
|
const listener = { notify: deliver, ready: false };
|
|
4715
5006
|
roomDraftListeners.add(listener);
|
|
@@ -4758,12 +5049,13 @@ export function composeRuntime(options) {
|
|
|
4758
5049
|
};
|
|
4759
5050
|
}
|
|
4760
5051
|
if (topic === 'readiness') {
|
|
4761
|
-
return watchReadiness({ target: '', code: 'stored',
|
|
5052
|
+
return watchReadiness({ target: '', code: 'stored', space: null }, null, deliver);
|
|
4762
5053
|
}
|
|
4763
5054
|
if (topic === 'rooms') {
|
|
4764
|
-
|
|
4765
|
-
|
|
5055
|
+
unlistedRooms.add(deliver);
|
|
5056
|
+
emitKnownRooms();
|
|
4766
5057
|
return () => {
|
|
5058
|
+
unlistedRooms.delete(deliver);
|
|
4767
5059
|
roomListeners.delete(deliver);
|
|
4768
5060
|
};
|
|
4769
5061
|
}
|
|
@@ -4802,6 +5094,13 @@ export function composeRuntime(options) {
|
|
|
4802
5094
|
memberListeners.delete(deliver);
|
|
4803
5095
|
};
|
|
4804
5096
|
}
|
|
5097
|
+
if (topic === 'authority-changes') {
|
|
5098
|
+
authorityChangeListeners.add(deliver);
|
|
5099
|
+
deliver(authorityChanges);
|
|
5100
|
+
return () => {
|
|
5101
|
+
authorityChangeListeners.delete(deliver);
|
|
5102
|
+
};
|
|
5103
|
+
}
|
|
4805
5104
|
if (topic === 'queue') {
|
|
4806
5105
|
const queue = { locator: input[0], notify: deliver };
|
|
4807
5106
|
queueSubscriptions.add(queue);
|
|
@@ -4810,24 +5109,34 @@ export function composeRuntime(options) {
|
|
|
4810
5109
|
queueSubscriptions.delete(queue);
|
|
4811
5110
|
};
|
|
4812
5111
|
}
|
|
4813
|
-
return watchPlan(planned(input[0], null), deliver);
|
|
5112
|
+
return watchPlan(planned(input[0], null), deliver, fail);
|
|
4814
5113
|
};
|
|
4815
5114
|
const md = {
|
|
4816
5115
|
loaded() {
|
|
4817
5116
|
return openLocal().then(() => undefined);
|
|
4818
5117
|
},
|
|
4819
5118
|
rooms() {
|
|
4820
|
-
|
|
5119
|
+
if (closed)
|
|
5120
|
+
return Promise.resolve([]);
|
|
5121
|
+
return new Promise((settle) => {
|
|
5122
|
+
const answer = (value) => {
|
|
5123
|
+
unlistedRooms.delete(answer);
|
|
5124
|
+
roomListeners.delete(answer);
|
|
5125
|
+
settle(value);
|
|
5126
|
+
};
|
|
5127
|
+
unlistedRooms.add(answer);
|
|
5128
|
+
emitKnownRooms();
|
|
5129
|
+
});
|
|
4821
5130
|
},
|
|
4822
5131
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
4823
|
-
search: (input) => searched(input, null
|
|
5132
|
+
search: (input) => searched(input, null),
|
|
4824
5133
|
types(locator) {
|
|
4825
5134
|
return {
|
|
4826
5135
|
collection: modelTypes(modelOf(locator)),
|
|
4827
5136
|
typedSelection: rooms.get(locator)?.typedSelection === undefined ||
|
|
4828
5137
|
rooms.get(locator)?.typedSelection === null
|
|
4829
5138
|
? null
|
|
4830
|
-
: modelTypes(
|
|
5139
|
+
: modelTypes(builtIn),
|
|
4831
5140
|
};
|
|
4832
5141
|
},
|
|
4833
5142
|
roomDraft: {
|
|
@@ -4844,33 +5153,17 @@ export function composeRuntime(options) {
|
|
|
4844
5153
|
const record = roomDrafts.get(room)?.get(path);
|
|
4845
5154
|
return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
|
|
4846
5155
|
},
|
|
4847
|
-
write(room, path, revision, input) {
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
const
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
? undefined
|
|
4859
|
-
: await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
|
|
4860
|
-
const record = {
|
|
4861
|
-
locator: room,
|
|
4862
|
-
path,
|
|
4863
|
-
revision: roomDraftRevision(),
|
|
4864
|
-
parts: held(current?.parts, parts, file),
|
|
4865
|
-
returned: current?.returned ?? null,
|
|
4866
|
-
};
|
|
4867
|
-
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
4868
|
-
? []
|
|
4869
|
-
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
4870
|
-
rememberRoomDraft(record);
|
|
4871
|
-
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
4872
|
-
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
4873
|
-
});
|
|
5156
|
+
write(room, path, revision, input, next = roomDraftRevision()) {
|
|
5157
|
+
if (typeof next !== 'string' || next === '')
|
|
5158
|
+
return Promise.reject(runtimeError('room-draft-malformed'));
|
|
5159
|
+
return gathered(`room-draft\u0000${room}\u0000${path}`, { revision, input, next }, (earlier, later) => {
|
|
5160
|
+
if (later.revision !== earlier.next)
|
|
5161
|
+
return null;
|
|
5162
|
+
const parts = gatheredParts(earlier.input, later.input);
|
|
5163
|
+
return parts === null
|
|
5164
|
+
? null
|
|
5165
|
+
: { revision: earlier.revision, input: parts, next: later.next };
|
|
5166
|
+
}, (written) => writtenRoomDraft(room, path, written.revision, written.input, written.next));
|
|
4874
5167
|
},
|
|
4875
5168
|
discard(room, path, revision) {
|
|
4876
5169
|
return exclusive(async () => {
|
|
@@ -4897,13 +5190,13 @@ export function composeRuntime(options) {
|
|
|
4897
5190
|
});
|
|
4898
5191
|
},
|
|
4899
5192
|
write(app, value) {
|
|
4900
|
-
return
|
|
5193
|
+
return exclusive(() => keptPresentation(app, () => structuredClone(value)));
|
|
4901
5194
|
},
|
|
4902
5195
|
edit(app, members) {
|
|
4903
|
-
return
|
|
4904
|
-
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(
|
|
4905
|
-
...Object.entries(
|
|
4906
|
-
]));
|
|
5196
|
+
return gathered(`presentation\u0000${app}`, structuredClone(members), (earlier, later) => ({ ...earlier, ...later }), (edited) => keptPresentation(app, (held) => Object.fromEntries([
|
|
5197
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(edited, member)),
|
|
5198
|
+
...Object.entries(edited).filter(([, value]) => value !== null),
|
|
5199
|
+
])));
|
|
4907
5200
|
},
|
|
4908
5201
|
},
|
|
4909
5202
|
save(room, named, writtenBy) {
|
|
@@ -4922,7 +5215,8 @@ export function composeRuntime(options) {
|
|
|
4922
5215
|
part: readPart,
|
|
4923
5216
|
read: readBinaryPath,
|
|
4924
5217
|
link: linkBinaryPath,
|
|
4925
|
-
|
|
5218
|
+
show: showBinaryPath,
|
|
5219
|
+
attach: attachBinary,
|
|
4926
5220
|
},
|
|
4927
5221
|
async call(name, input) {
|
|
4928
5222
|
await ready;
|
|
@@ -4943,9 +5237,10 @@ export function composeRuntime(options) {
|
|
|
4943
5237
|
},
|
|
4944
5238
|
async retry() {
|
|
4945
5239
|
journal.record('retry');
|
|
4946
|
-
|
|
5240
|
+
unpreparable.clear();
|
|
4947
5241
|
queueReadiness();
|
|
4948
|
-
|
|
5242
|
+
if (full)
|
|
5243
|
+
await probed();
|
|
4949
5244
|
},
|
|
4950
5245
|
observe: observing(watch),
|
|
4951
5246
|
async reauthorize() {
|
|
@@ -4959,7 +5254,7 @@ export function composeRuntime(options) {
|
|
|
4959
5254
|
authorization = undefined;
|
|
4960
5255
|
discoveryPaused = false;
|
|
4961
5256
|
if (options.adapter.roomStream === undefined)
|
|
4962
|
-
await sync('unstreamed');
|
|
5257
|
+
await sync('unstreamed', null);
|
|
4963
5258
|
else
|
|
4964
5259
|
await task(OPENING_LANE, ensureAuthorization);
|
|
4965
5260
|
},
|
|
@@ -4967,7 +5262,6 @@ export function composeRuntime(options) {
|
|
|
4967
5262
|
const reachable = {
|
|
4968
5263
|
md,
|
|
4969
5264
|
watch,
|
|
4970
|
-
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
4971
5265
|
plan: planned,
|
|
4972
5266
|
query: (plan) => rowsQuery(plan),
|
|
4973
5267
|
watchPlan,
|
|
@@ -4977,12 +5271,12 @@ export function composeRuntime(options) {
|
|
|
4977
5271
|
lockedRooms,
|
|
4978
5272
|
watchLocks,
|
|
4979
5273
|
located,
|
|
5274
|
+
room: (locator) => listed().find((one) => one.locator === locator),
|
|
4980
5275
|
};
|
|
4981
5276
|
return {
|
|
4982
5277
|
md,
|
|
4983
5278
|
seed,
|
|
4984
5279
|
stop,
|
|
4985
|
-
lose: loseRooms,
|
|
4986
5280
|
watch,
|
|
4987
5281
|
persisted(answer) {
|
|
4988
5282
|
if (answer === persistence)
|
|
@@ -4991,6 +5285,10 @@ export function composeRuntime(options) {
|
|
|
4991
5285
|
journal.record(`persistence ${answer}`);
|
|
4992
5286
|
queueReadiness();
|
|
4993
5287
|
},
|
|
4994
|
-
within: (reach, port) =>
|
|
5288
|
+
within: (reach, port) => {
|
|
5289
|
+
if (full)
|
|
5290
|
+
void probed();
|
|
5291
|
+
return narrowed(reachable, reach, port);
|
|
5292
|
+
},
|
|
4995
5293
|
};
|
|
4996
5294
|
}
|