@agentshouse/mdruntime 0.1.0-ce15a88ae8478 → 0.1.0-ce41fec66d91b
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 +10 -8
- package/dist/adapter.d.ts +71 -59
- 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/client.d.ts +8 -10
- 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.map +1 -1
- package/dist/compile.js +95 -30
- package/dist/connection.d.ts +8 -8
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1304 -1071
- package/dist/conversation.d.ts +13 -11
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +244 -136
- 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 +1 -1
- package/dist/file.d.ts +2 -11
- package/dist/file.d.ts.map +1 -1
- package/dist/file.js +6 -4
- package/dist/fulltext.d.ts.map +1 -1
- package/dist/fulltext.js +8 -6
- package/dist/host.d.ts +3 -4
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +40 -20
- package/dist/index.d.ts +10 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- 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 +9 -6
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +175 -98
- 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 +1 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +1 -1
- 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/query.d.ts +6 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/reach.d.ts +8 -3
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +67 -51
- package/dist/readiness.d.ts +13 -14
- 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 +85 -30
- package/dist/replica.d.ts +5 -5
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +4 -7
- package/dist/room-draft.d.ts +18 -2
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +73 -6
- package/dist/rows.d.ts +15 -6
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +102 -74
- package/dist/save.d.ts +2 -0
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +13 -7
- 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 +20 -26
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +27 -51
- package/dist/stored.d.ts +10 -18
- package/dist/stored.d.ts.map +1 -1
- package/dist/stored.js +209 -193
- package/dist/tables.d.ts +28 -35
- package/dist/tables.d.ts.map +1 -1
- package/dist/tables.js +58 -50
- 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 +5 -9
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +2 -3
- package/dist/words.d.ts.map +1 -1
- package/dist/words.js +8 -2
- package/package.json +11 -18
- package/AGENTS.md +0 -319
- 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/context.js +0 -10
- 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 -254
- package/dist/chat/messages.d.ts +0 -35
- package/dist/chat/messages.d.ts.map +0 -1
- package/dist/chat/messages.js +0 -210
- 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 -70
- package/dist/chat/provider.d.ts.map +0 -1
- package/dist/chat/provider.js +0 -635
- 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 -307
- 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,5 +1,5 @@
|
|
|
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
5
|
import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
|
|
@@ -12,29 +12,28 @@ 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
39
|
function within(root, path) {
|
|
@@ -55,34 +54,43 @@ const CONVERSATION_LANE = 'conversations';
|
|
|
55
54
|
const ROOM_READS = 256;
|
|
56
55
|
const ROOM_ATTEMPTS = 16;
|
|
57
56
|
const ERASE_BATCH = 512;
|
|
58
|
-
const
|
|
59
|
-
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;
|
|
60
63
|
const QUERY_BYTES = 1 << 20;
|
|
61
64
|
const STAGE_BYTES = 1 << 20;
|
|
62
|
-
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;
|
|
63
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.
|
|
64
74
|
const LOCK_GRACE_MS = 120_000;
|
|
65
75
|
function operationOf(input) {
|
|
66
76
|
const operation = input?.operation_id;
|
|
67
77
|
return typeof operation === 'string' ? operation : null;
|
|
68
78
|
}
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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));
|
|
75
86
|
});
|
|
76
87
|
}
|
|
77
|
-
function roomLane(locator) {
|
|
78
|
-
return `room ${locator}`;
|
|
79
|
-
}
|
|
80
88
|
function pendingKey(locator, id) {
|
|
81
89
|
return `${locator}\u0000${id}`;
|
|
82
90
|
}
|
|
83
91
|
export function composeRuntime(options) {
|
|
84
92
|
const localSaves = options.adapter.localSaves === true;
|
|
85
|
-
const roomTypes = roomTypesOf(options.adapter.roomTypes);
|
|
93
|
+
const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
|
|
86
94
|
let authorization = options.authorization;
|
|
87
95
|
let opening;
|
|
88
96
|
const fences = new Map();
|
|
@@ -92,44 +100,51 @@ export function composeRuntime(options) {
|
|
|
92
100
|
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
93
101
|
let discoveries = 0;
|
|
94
102
|
const replicas = new FencedMap(isFenced);
|
|
103
|
+
const reopened = new Set();
|
|
104
|
+
const usableWhenHeld = options.usableWhenHeld ?? [];
|
|
95
105
|
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
96
106
|
const pendings = new Map();
|
|
97
107
|
const seeds = new Map();
|
|
98
108
|
const seedWatchers = new Map();
|
|
99
109
|
const roomDrafts = new FencedMap(isFenced);
|
|
110
|
+
const attachedFiles = new FencedMap(isFenced);
|
|
100
111
|
const presentations = new Map();
|
|
101
112
|
let limits = [];
|
|
102
113
|
let capabilities = [];
|
|
103
114
|
const rooms = new FencedMap(isFenced);
|
|
104
115
|
const counted = new Map();
|
|
105
116
|
const unreadable = new Set();
|
|
106
|
-
const
|
|
107
|
-
const models = new FencedMap(
|
|
108
|
-
const liveModels = new FencedMap(
|
|
117
|
+
const builtIn = builtInModel(roomTypes);
|
|
118
|
+
const models = new FencedMap(isFenced);
|
|
119
|
+
const liveModels = new FencedMap(isFenced);
|
|
109
120
|
const invalidModels = new Set();
|
|
110
121
|
const discovered = new Map();
|
|
111
|
-
|
|
122
|
+
let inFlight = new AbortController();
|
|
123
|
+
const refreshed = options.adapter.refresh?.bind(options.adapter);
|
|
124
|
+
const reads = {
|
|
125
|
+
bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
|
|
126
|
+
...(refreshed === undefined
|
|
127
|
+
? {}
|
|
128
|
+
: {
|
|
129
|
+
refresh: (authorized, request) => readUntil(refreshed(authorized, request), inFlight.signal),
|
|
130
|
+
}),
|
|
131
|
+
reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
|
|
132
|
+
};
|
|
112
133
|
let roomsListed = false;
|
|
113
134
|
const scopedRefusals = new Map();
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
function modelKey(locator, attached) {
|
|
117
|
-
return `${attached} ${locator}`;
|
|
118
|
-
}
|
|
119
|
-
function modelOf(locator, attached = 'collection') {
|
|
120
|
-
return models.get(modelKey(locator, attached)) ?? NO_MODEL;
|
|
135
|
+
function modelOf(locator) {
|
|
136
|
+
return models.get(locator) ?? NO_MODEL;
|
|
121
137
|
}
|
|
122
|
-
function savedModel(locator
|
|
123
|
-
return invalidModels.has(
|
|
138
|
+
function savedModel(locator) {
|
|
139
|
+
return invalidModels.has(locator)
|
|
124
140
|
? { status: 'unavailable', reason: 'revoked' }
|
|
125
|
-
: modelOf(locator
|
|
141
|
+
: modelOf(locator);
|
|
142
|
+
}
|
|
143
|
+
function composed(record) {
|
|
144
|
+
return compositionOf(record, builtIn);
|
|
126
145
|
}
|
|
127
146
|
function compositionFor(record) {
|
|
128
|
-
return
|
|
129
|
-
...record,
|
|
130
|
-
model: savedModel(record.locator, 'collection'),
|
|
131
|
-
selectionModel: savedModel(record.locator, 'typed-selection'),
|
|
132
|
-
});
|
|
147
|
+
return composed({ ...record, model: savedModel(record.locator) });
|
|
133
148
|
}
|
|
134
149
|
let refusal = null;
|
|
135
150
|
let storageLoss = null;
|
|
@@ -139,10 +154,10 @@ export function composeRuntime(options) {
|
|
|
139
154
|
let closed = false;
|
|
140
155
|
let stopping = false;
|
|
141
156
|
let withdrawStream;
|
|
142
|
-
const announced = new Map();
|
|
143
157
|
let underway = null;
|
|
144
158
|
let streamed = null;
|
|
145
159
|
let handed = false;
|
|
160
|
+
let naming = 0;
|
|
146
161
|
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
147
162
|
const repairing = new Set();
|
|
148
163
|
const calling = new Set();
|
|
@@ -153,15 +168,18 @@ export function composeRuntime(options) {
|
|
|
153
168
|
let recorded = false;
|
|
154
169
|
const stale = new Set();
|
|
155
170
|
let generation = 0;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
const reloadsDue = new Map();
|
|
171
|
+
const reloadsDue = new Set();
|
|
172
|
+
const highest = new Map();
|
|
173
|
+
const beyondHorizon = new Set();
|
|
160
174
|
const lanes = new Map();
|
|
161
|
-
|
|
175
|
+
const roomLanes = new Map();
|
|
176
|
+
let roomWork = 0;
|
|
177
|
+
const roomReads = { free: ROOM_READS, waiting: [] };
|
|
162
178
|
const working = new Set();
|
|
163
179
|
let mutations = Promise.resolve();
|
|
180
|
+
let gathering = null;
|
|
164
181
|
const roomListeners = new Set();
|
|
182
|
+
const unlistedRooms = new Set();
|
|
165
183
|
const refusalListeners = new Set();
|
|
166
184
|
const storageLossListeners = new Set();
|
|
167
185
|
const statusListeners = new Set();
|
|
@@ -172,6 +190,7 @@ export function composeRuntime(options) {
|
|
|
172
190
|
let withdrawConversations;
|
|
173
191
|
let currencyQueued = null;
|
|
174
192
|
let socketOpen = false;
|
|
193
|
+
let announced = false;
|
|
175
194
|
const confirmed = new Set();
|
|
176
195
|
let lockGrace = null;
|
|
177
196
|
const lockListeners = new Set();
|
|
@@ -181,49 +200,80 @@ export function composeRuntime(options) {
|
|
|
181
200
|
const serviceModeListeners = new Set();
|
|
182
201
|
let member = null;
|
|
183
202
|
const memberListeners = new Set();
|
|
184
|
-
const prepared = new Set();
|
|
185
203
|
const unpreparedDemand = new Set();
|
|
186
204
|
const unpreparable = new Set();
|
|
187
205
|
let preparing = false;
|
|
188
206
|
let targets = {};
|
|
189
207
|
let persistence = options.shell.persistence;
|
|
190
|
-
let
|
|
191
|
-
|
|
192
|
-
let
|
|
208
|
+
let full = false;
|
|
209
|
+
let probing = null;
|
|
210
|
+
let testing = null;
|
|
211
|
+
const database = {
|
|
212
|
+
async ask(request) {
|
|
213
|
+
try {
|
|
214
|
+
return await options.database.ask(request);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
if (refusedForSpace(error))
|
|
218
|
+
filled();
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
};
|
|
193
223
|
const readinessWatches = new Set();
|
|
194
224
|
let readinessQueued = false;
|
|
195
225
|
const catching = new Set();
|
|
196
226
|
const transfers = new FencedMap(isFenced);
|
|
227
|
+
const installs = new Set();
|
|
228
|
+
const arrivals = new Map();
|
|
229
|
+
const streaming = new Set();
|
|
230
|
+
const underived = new Map();
|
|
231
|
+
const unindexed = new Map();
|
|
232
|
+
const startedOver = new Map();
|
|
233
|
+
let arrivalReported = 0;
|
|
234
|
+
let streamsFree = STREAMS_HELD;
|
|
235
|
+
let queuedText = 0;
|
|
236
|
+
const installWaiters = [];
|
|
237
|
+
const stageBudget = {
|
|
238
|
+
bytes: STAGE_FLOOR * 4,
|
|
239
|
+
floor: STAGE_FLOOR,
|
|
240
|
+
ceiling: STAGE_BYTES,
|
|
241
|
+
aim: STAGE_AIM_MS,
|
|
242
|
+
};
|
|
243
|
+
const deriveBudget = {
|
|
244
|
+
bytes: DERIVE_FLOOR * 4,
|
|
245
|
+
floor: DERIVE_FLOOR,
|
|
246
|
+
ceiling: DERIVE_BYTES,
|
|
247
|
+
aim: DERIVE_AIM_MS,
|
|
248
|
+
};
|
|
249
|
+
const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
|
|
197
250
|
const conversations = createConversationStore({
|
|
198
|
-
agents: declaredAgents,
|
|
199
|
-
documents: (declaration, after) => rowsQuery(planOf({
|
|
200
|
-
rooms: [declaration.locator],
|
|
201
|
-
types: [declaration.type],
|
|
202
|
-
select: {
|
|
203
|
-
fields: [
|
|
204
|
-
declaration.identity,
|
|
205
|
-
declaration.name,
|
|
206
|
-
...(declaration.retired === undefined ? [] : [declaration.retired]),
|
|
207
|
-
].map((field) => `/${escapePointer(field)}`),
|
|
208
|
-
},
|
|
209
|
-
limit: MAXIMUM_WINDOW,
|
|
210
|
-
...(after === undefined ? {} : { after }),
|
|
211
|
-
}, roomFacts())),
|
|
212
251
|
call: (name, input) => invokeAction(name, input),
|
|
252
|
+
history: (request) => readConversationHistory(request),
|
|
213
253
|
durably,
|
|
214
254
|
stream: () => options.adapter.conversations,
|
|
215
255
|
publish: publishChangeSet,
|
|
216
256
|
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
257
|
+
full: () => full,
|
|
217
258
|
closed: () => closed,
|
|
218
259
|
unanswered: resendLater,
|
|
219
260
|
expired: (operation) => pastHorizon(operation),
|
|
220
261
|
});
|
|
221
262
|
const journal = createLog();
|
|
222
|
-
const keptFiles = openedFiles(
|
|
223
|
-
let
|
|
263
|
+
const keptFiles = openedFiles(database, isFenced);
|
|
264
|
+
let readyFromStore = () => { };
|
|
224
265
|
const ready = new Promise((resolve) => {
|
|
225
|
-
|
|
266
|
+
readyFromStore = resolve;
|
|
267
|
+
});
|
|
268
|
+
let notifyDiscovered = () => { };
|
|
269
|
+
const firstDiscovery = new Promise((resolve) => {
|
|
270
|
+
notifyDiscovered = resolve;
|
|
226
271
|
});
|
|
272
|
+
let roomsNamed = false;
|
|
273
|
+
function notifyReady() {
|
|
274
|
+
notifyDiscovered();
|
|
275
|
+
readyFromStore();
|
|
276
|
+
}
|
|
227
277
|
function rememberRoomDraft(record) {
|
|
228
278
|
const room = roomDrafts.get(record.locator) ?? new Map();
|
|
229
279
|
room.set(record.path, record);
|
|
@@ -243,7 +293,7 @@ export function composeRuntime(options) {
|
|
|
243
293
|
return undefined;
|
|
244
294
|
const local = await openLocal();
|
|
245
295
|
const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
|
|
246
|
-
return { ...slice, collection: collectionOf(slice.documents,
|
|
296
|
+
return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
|
|
247
297
|
}
|
|
248
298
|
function roomDraftOf(record, visible) {
|
|
249
299
|
return structuredClone({
|
|
@@ -299,14 +349,40 @@ export function composeRuntime(options) {
|
|
|
299
349
|
const value = presentations.get(app);
|
|
300
350
|
return value === undefined ? null : structuredClone(value);
|
|
301
351
|
}
|
|
302
|
-
function
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
352
|
+
async function writtenRoomDraft(room, path, revision, input, next) {
|
|
353
|
+
const parts = readParts(input);
|
|
354
|
+
if (path === '' || normalize(path) !== path)
|
|
355
|
+
throw runtimeError('room-draft-malformed');
|
|
356
|
+
const local = await openLocal();
|
|
357
|
+
const current = roomDrafts.get(room)?.get(path);
|
|
358
|
+
if ((current?.revision ?? null) !== revision) {
|
|
359
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
360
|
+
}
|
|
361
|
+
const file = parts.file === undefined
|
|
362
|
+
? undefined
|
|
363
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined
|
|
364
|
+
? binaryBase(await visibleOf(room, [path]), path)
|
|
365
|
+
: parts.file.base);
|
|
366
|
+
const record = {
|
|
367
|
+
locator: room,
|
|
368
|
+
path,
|
|
369
|
+
revision: next,
|
|
370
|
+
parts: held(current?.parts, parts, file),
|
|
371
|
+
returned: current?.returned ?? null,
|
|
372
|
+
};
|
|
373
|
+
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
374
|
+
? []
|
|
375
|
+
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
376
|
+
rememberRoomDraft(record);
|
|
377
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
378
|
+
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
379
|
+
}
|
|
380
|
+
async function keptPresentation(app, next) {
|
|
381
|
+
const local = await openLocal();
|
|
382
|
+
const stored = next(presentations.get(app));
|
|
383
|
+
await local.present({ app, value: stored });
|
|
384
|
+
presentations.set(app, stored);
|
|
385
|
+
publishChangeSet(changeSet('', { presentation: app }));
|
|
310
386
|
}
|
|
311
387
|
function emitPresentation(app) {
|
|
312
388
|
if (closed)
|
|
@@ -321,6 +397,20 @@ export function composeRuntime(options) {
|
|
|
321
397
|
(queuesWithoutReplica.size > 0 ||
|
|
322
398
|
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
323
399
|
}
|
|
400
|
+
function unsent() {
|
|
401
|
+
const changed = new Map();
|
|
402
|
+
for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
403
|
+
for (const { batch, state } of entriesOf(room)) {
|
|
404
|
+
if (state === 'local')
|
|
405
|
+
continue;
|
|
406
|
+
for (const { path } of batch.changes)
|
|
407
|
+
changed.set(`${room}\u0000${path}`, { room, path });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return [...changed]
|
|
411
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
412
|
+
.map(([, change]) => change);
|
|
413
|
+
}
|
|
324
414
|
function attention() {
|
|
325
415
|
const attentionRequired = [];
|
|
326
416
|
for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
|
|
@@ -332,16 +422,9 @@ export function composeRuntime(options) {
|
|
|
332
422
|
returned: structuredClone(record.returned),
|
|
333
423
|
});
|
|
334
424
|
}
|
|
335
|
-
for (const [room, refusal] of sourceRefusals)
|
|
336
|
-
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
337
425
|
attentionRequired.sort((left, right) => {
|
|
338
426
|
if (left.room !== right.room)
|
|
339
427
|
return left.room < right.room ? -1 : 1;
|
|
340
|
-
if ('refusal' in left || 'refusal' in right) {
|
|
341
|
-
if ('refusal' in left && 'refusal' in right)
|
|
342
|
-
return 0;
|
|
343
|
-
return 'refusal' in left ? -1 : 1;
|
|
344
|
-
}
|
|
345
428
|
if (left.path !== right.path)
|
|
346
429
|
return left.path < right.path ? -1 : 1;
|
|
347
430
|
return 0;
|
|
@@ -353,7 +436,7 @@ export function composeRuntime(options) {
|
|
|
353
436
|
if (reset === null || submission.reset < reset)
|
|
354
437
|
reset = submission.reset;
|
|
355
438
|
}
|
|
356
|
-
return { attentionRequired, reset };
|
|
439
|
+
return { attentionRequired, unsent: unsent(), reset };
|
|
357
440
|
}
|
|
358
441
|
function documentsIn(locator) {
|
|
359
442
|
return replicas.get(locator)?.count ?? 0;
|
|
@@ -426,7 +509,7 @@ export function composeRuntime(options) {
|
|
|
426
509
|
const governed = governs(reach, room);
|
|
427
510
|
const attached = [
|
|
428
511
|
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
429
|
-
...(governed.selection ? [
|
|
512
|
+
...(governed.selection ? [builtIn] : []),
|
|
430
513
|
];
|
|
431
514
|
return {
|
|
432
515
|
locator: room.locator,
|
|
@@ -442,11 +525,26 @@ export function composeRuntime(options) {
|
|
|
442
525
|
const value = listed();
|
|
443
526
|
for (const listener of roomListeners)
|
|
444
527
|
listener(value);
|
|
528
|
+
emitKnownRooms();
|
|
529
|
+
}
|
|
530
|
+
function emitKnownRooms() {
|
|
531
|
+
conversations.learned();
|
|
532
|
+
if (unlistedRooms.size === 0)
|
|
533
|
+
return;
|
|
534
|
+
if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
|
|
535
|
+
return;
|
|
536
|
+
const value = listed();
|
|
537
|
+
for (const listener of [...unlistedRooms]) {
|
|
538
|
+
unlistedRooms.delete(listener);
|
|
539
|
+
roomListeners.add(listener);
|
|
540
|
+
listener(value);
|
|
541
|
+
}
|
|
445
542
|
}
|
|
446
543
|
function emitRefusal() {
|
|
447
544
|
queueReadiness();
|
|
448
545
|
for (const listener of refusalListeners)
|
|
449
546
|
listener(refusal);
|
|
547
|
+
emitKnownRooms();
|
|
450
548
|
}
|
|
451
549
|
function emitStorageLoss() {
|
|
452
550
|
queueReadiness();
|
|
@@ -467,6 +565,7 @@ export function composeRuntime(options) {
|
|
|
467
565
|
if (closed)
|
|
468
566
|
return;
|
|
469
567
|
queueReadiness();
|
|
568
|
+
emitKnownRooms();
|
|
470
569
|
const next = status.state === 'synced' && !conversations.current()
|
|
471
570
|
? { ...status, state: 'syncing' }
|
|
472
571
|
: status;
|
|
@@ -480,9 +579,6 @@ export function composeRuntime(options) {
|
|
|
480
579
|
if (localSaves)
|
|
481
580
|
return Object.freeze({ batch, state: 'local' });
|
|
482
581
|
const state = pendings.get(pendingKey(locator, batch.id));
|
|
483
|
-
if (state?.accepted !== undefined) {
|
|
484
|
-
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
485
|
-
}
|
|
486
582
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
487
583
|
}
|
|
488
584
|
function entriesOf(locator) {
|
|
@@ -501,18 +597,25 @@ export function composeRuntime(options) {
|
|
|
501
597
|
function unpublished(error) {
|
|
502
598
|
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
503
599
|
}
|
|
600
|
+
function indexed(locator) {
|
|
601
|
+
return replicas.get(locator)?.indexed === true;
|
|
602
|
+
}
|
|
504
603
|
async function prepareReplica(locator) {
|
|
505
604
|
for (;;) {
|
|
506
605
|
const held = replicas.get(locator);
|
|
507
|
-
if (held === undefined || closed ||
|
|
606
|
+
if (held === undefined || closed || held.indexed)
|
|
508
607
|
return;
|
|
509
608
|
const key = held.record.key;
|
|
510
|
-
const done = await exclusive(async () =>
|
|
511
|
-
|
|
609
|
+
const done = await exclusive(async () => {
|
|
610
|
+
const local = await openLocal();
|
|
611
|
+
return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
|
|
612
|
+
});
|
|
613
|
+
const now = replicas.get(locator);
|
|
614
|
+
if (now?.record.key !== key)
|
|
512
615
|
continue;
|
|
513
|
-
if (!done)
|
|
616
|
+
if (!done.done)
|
|
514
617
|
continue;
|
|
515
|
-
|
|
618
|
+
replicas.set(locator, { ...now, indexed: true });
|
|
516
619
|
publishChangeSet(changeSet(locator, { documents: null }));
|
|
517
620
|
liveQueries.recover();
|
|
518
621
|
queueReadiness();
|
|
@@ -520,7 +623,7 @@ export function composeRuntime(options) {
|
|
|
520
623
|
}
|
|
521
624
|
}
|
|
522
625
|
function unprepared() {
|
|
523
|
-
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !
|
|
626
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
|
|
524
627
|
}
|
|
525
628
|
function unpreparedBy(locator, error) {
|
|
526
629
|
unpreparable.add(locator);
|
|
@@ -581,14 +684,14 @@ export function composeRuntime(options) {
|
|
|
581
684
|
pending.push({ room: locator, awaits: 'acquisition' });
|
|
582
685
|
continue;
|
|
583
686
|
}
|
|
584
|
-
if (!
|
|
687
|
+
if (!replica.indexed) {
|
|
585
688
|
pending.push({ room: locator, awaits: 'preparation' });
|
|
586
689
|
continue;
|
|
587
690
|
}
|
|
588
691
|
coverage.push({
|
|
589
692
|
room: locator,
|
|
590
693
|
generation: replica.record.transfer,
|
|
591
|
-
current: !(catching.has(locator) ||
|
|
694
|
+
current: !(catching.has(locator) || transfers.has(locator)),
|
|
592
695
|
});
|
|
593
696
|
read.push({ locator, key: replica.record.key });
|
|
594
697
|
for (const batch of replica.queue)
|
|
@@ -615,8 +718,6 @@ export function composeRuntime(options) {
|
|
|
615
718
|
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
616
719
|
}
|
|
617
720
|
function roomState(locator) {
|
|
618
|
-
if (sourceRefusals.has(locator))
|
|
619
|
-
return 'refused';
|
|
620
721
|
const listedRoom = discovered.get(locator);
|
|
621
722
|
if (roomsListed &&
|
|
622
723
|
(listedRoom === undefined ||
|
|
@@ -625,13 +726,13 @@ export function composeRuntime(options) {
|
|
|
625
726
|
return 'unadmitted';
|
|
626
727
|
if (!replicas.has(locator))
|
|
627
728
|
return 'acquiring';
|
|
628
|
-
if (!
|
|
729
|
+
if (!indexed(locator))
|
|
629
730
|
return 'preparing';
|
|
630
731
|
return 'complete';
|
|
631
732
|
}
|
|
632
733
|
function conversationCoverage(reach) {
|
|
633
734
|
const declared = reach === null || reach.conversations;
|
|
634
|
-
if (!declared || options.adapter.conversations === undefined
|
|
735
|
+
if (!declared || options.adapter.conversations === undefined)
|
|
635
736
|
return 'undeclared';
|
|
636
737
|
if (!conversations.known() || !conversations.current())
|
|
637
738
|
return 'acquiring';
|
|
@@ -646,21 +747,47 @@ export function composeRuntime(options) {
|
|
|
646
747
|
? 'connected'
|
|
647
748
|
: 'connecting';
|
|
648
749
|
}
|
|
649
|
-
function
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
750
|
+
function bytesOf(room, underway) {
|
|
751
|
+
const transfer = transfers.get(room);
|
|
752
|
+
if (transfer !== undefined) {
|
|
753
|
+
const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
|
|
754
|
+
return {
|
|
755
|
+
done: transfer.done.bytes - Math.ceil(owed),
|
|
756
|
+
total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
|
|
760
|
+
return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
|
|
761
|
+
}
|
|
762
|
+
function installationOf(coverage) {
|
|
763
|
+
const rooms = { done: 0, total: 0 };
|
|
764
|
+
const bytes = { done: 0, total: 0 };
|
|
765
|
+
let installing = 0;
|
|
766
|
+
let downloading = false;
|
|
767
|
+
for (const { room, state } of coverage) {
|
|
768
|
+
if (state === 'unadmitted')
|
|
769
|
+
continue;
|
|
770
|
+
rooms.total += 1;
|
|
771
|
+
if (state === 'complete')
|
|
772
|
+
rooms.done += 1;
|
|
773
|
+
const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
|
|
774
|
+
if (underway)
|
|
775
|
+
installing += 1;
|
|
776
|
+
if (underway && (streaming.has(room) || !arrivals.has(room)))
|
|
777
|
+
downloading = true;
|
|
778
|
+
const now = bytesOf(room, underway);
|
|
779
|
+
const earlier = underway ? startedOver.get(room) : undefined;
|
|
780
|
+
bytes.done += Math.max(now.done, earlier?.done ?? 0);
|
|
781
|
+
bytes.total += Math.max(now.total, earlier?.total ?? 0);
|
|
782
|
+
}
|
|
783
|
+
const step = installing === 0
|
|
784
|
+
? null
|
|
785
|
+
: downloading
|
|
786
|
+
? 'downloading'
|
|
787
|
+
: bytes.done < bytes.total
|
|
788
|
+
? 'writing'
|
|
789
|
+
: 'replaying';
|
|
790
|
+
return { step, rooms, bytes };
|
|
664
791
|
}
|
|
665
792
|
function readinessFor(port, reach) {
|
|
666
793
|
const required = requiredRooms(reach);
|
|
@@ -682,18 +809,17 @@ export function composeRuntime(options) {
|
|
|
682
809
|
persistence,
|
|
683
810
|
connection: connectionOf(),
|
|
684
811
|
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
? capacity
|
|
688
|
-
: port.capacity,
|
|
689
|
-
refused: refusedIn(required.rooms),
|
|
812
|
+
space: full ? 'full' : port.space,
|
|
813
|
+
installation: installationOf(coverage),
|
|
690
814
|
lost: storageLoss !== null,
|
|
691
|
-
established: targets[port.target]?.established === true
|
|
815
|
+
established: targets[port.target]?.established === true ||
|
|
816
|
+
(usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
|
|
692
817
|
});
|
|
693
818
|
}
|
|
694
819
|
function establish(name) {
|
|
695
820
|
targets = { ...targets, [name]: { established: true } };
|
|
696
821
|
queueReadiness();
|
|
822
|
+
probeEachMinute();
|
|
697
823
|
void durably((local) => local.fact('targets', targets)).catch(() => {
|
|
698
824
|
journal.record(`target record failed ${name}`);
|
|
699
825
|
});
|
|
@@ -731,87 +857,56 @@ export function composeRuntime(options) {
|
|
|
731
857
|
readinessWatches.delete(watch);
|
|
732
858
|
};
|
|
733
859
|
}
|
|
734
|
-
function
|
|
735
|
-
|
|
736
|
-
for (const { path, content } of page.entries)
|
|
737
|
-
bytes += (path.length + (content?.length ?? 0)) * 2;
|
|
738
|
-
return bytes;
|
|
739
|
-
}
|
|
740
|
-
function exceeded(error) {
|
|
741
|
-
return (typeof error === 'object' &&
|
|
742
|
-
error !== null &&
|
|
743
|
-
error.name === 'QuotaExceededError');
|
|
744
|
-
}
|
|
745
|
-
async function estimated() {
|
|
746
|
-
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
747
|
-
return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
|
|
748
|
-
}
|
|
749
|
-
function pause(locator, cause) {
|
|
750
|
-
paused.add(locator);
|
|
751
|
-
capacity = cause;
|
|
752
|
-
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
753
|
-
queueReadiness();
|
|
754
|
-
if (rechecking !== null)
|
|
860
|
+
function filled() {
|
|
861
|
+
if (full || closed)
|
|
755
862
|
return;
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
863
|
+
full = true;
|
|
864
|
+
journal.record('space full');
|
|
865
|
+
queueReadiness();
|
|
866
|
+
probeEachMinute();
|
|
760
867
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
return;
|
|
765
|
-
const available = await estimated().catch(() => null);
|
|
766
|
-
if (available === null || available < (held.needed ?? 0))
|
|
767
|
-
return;
|
|
768
|
-
if (held.cause === 'storage-exceeded' &&
|
|
769
|
-
(held.available === null || available <= held.available))
|
|
770
|
-
return;
|
|
771
|
-
resume();
|
|
868
|
+
function installedTarget() {
|
|
869
|
+
return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
|
|
870
|
+
Object.values(targets).some((target) => target.established));
|
|
772
871
|
}
|
|
773
|
-
function
|
|
774
|
-
if (
|
|
775
|
-
clearInterval(rechecking);
|
|
776
|
-
rechecking = null;
|
|
777
|
-
if (paused.size === 0 && capacity === null)
|
|
872
|
+
function probeEachMinute() {
|
|
873
|
+
if (!full || probing !== null || closed || !installedTarget())
|
|
778
874
|
return;
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
if (!closed && authorization !== undefined)
|
|
784
|
-
void sync('unstreamed');
|
|
785
|
-
}
|
|
786
|
-
async function reclaimed(local) {
|
|
787
|
-
const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
|
|
788
|
-
for (const locator of unreferenced) {
|
|
789
|
-
await local.abandon(locator);
|
|
790
|
-
transfers.delete(locator);
|
|
791
|
-
}
|
|
792
|
-
if (unreferenced.length > 0)
|
|
793
|
-
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
794
|
-
return unreferenced.length > 0;
|
|
875
|
+
// 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.
|
|
876
|
+
probing = setInterval(() => {
|
|
877
|
+
void probed();
|
|
878
|
+
}, 60_000);
|
|
795
879
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
if (available === null || available >= needed)
|
|
880
|
+
function probed() {
|
|
881
|
+
testing ??= database.ask({ kind: 'space.probe' }).then(() => {
|
|
882
|
+
testing = null;
|
|
883
|
+
spaceFreed();
|
|
801
884
|
return true;
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
return false;
|
|
885
|
+
}, () => {
|
|
886
|
+
testing = null;
|
|
887
|
+
return false;
|
|
888
|
+
});
|
|
889
|
+
return testing;
|
|
808
890
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
891
|
+
function spaceFreed() {
|
|
892
|
+
if (!full)
|
|
893
|
+
return;
|
|
894
|
+
full = false;
|
|
895
|
+
if (probing !== null)
|
|
896
|
+
clearInterval(probing);
|
|
897
|
+
probing = null;
|
|
898
|
+
unpreparable.clear();
|
|
899
|
+
journal.record('space freed');
|
|
900
|
+
queueReadiness();
|
|
901
|
+
if (closed || authorization === undefined)
|
|
902
|
+
return;
|
|
903
|
+
if (options.adapter.roomStream === undefined)
|
|
904
|
+
void sync('unstreamed', null);
|
|
905
|
+
else
|
|
906
|
+
void resynchronize();
|
|
812
907
|
}
|
|
813
908
|
function seedRoomHeld(room, version) {
|
|
814
|
-
return
|
|
909
|
+
return indexed(room) && reachedBy(room, version);
|
|
815
910
|
}
|
|
816
911
|
function seedHeld(at) {
|
|
817
912
|
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
@@ -934,6 +1029,10 @@ export function composeRuntime(options) {
|
|
|
934
1029
|
}
|
|
935
1030
|
function publishChangeSet(set) {
|
|
936
1031
|
applyChangeSet(set);
|
|
1032
|
+
for (const { locator, path } of set.documents) {
|
|
1033
|
+
if (path !== null)
|
|
1034
|
+
attachedFiles.get(locator)?.delete(path);
|
|
1035
|
+
}
|
|
937
1036
|
if (set.rooms.length > 0 ||
|
|
938
1037
|
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
939
1038
|
emitRooms();
|
|
@@ -952,6 +1051,8 @@ export function composeRuntime(options) {
|
|
|
952
1051
|
}
|
|
953
1052
|
async function refuse(reason, provider = null) {
|
|
954
1053
|
discoveryPaused = true;
|
|
1054
|
+
inFlight.abort();
|
|
1055
|
+
inFlight = new AbortController();
|
|
955
1056
|
try {
|
|
956
1057
|
if (reason === 'revoked-authorization') {
|
|
957
1058
|
await exclusive(async () => {
|
|
@@ -959,27 +1060,24 @@ export function composeRuntime(options) {
|
|
|
959
1060
|
const records = [...replicas].map(([locator, held]) => ({
|
|
960
1061
|
locator,
|
|
961
1062
|
held,
|
|
962
|
-
record: { ...held.record, model: revoked
|
|
1063
|
+
record: { ...held.record, model: revoked },
|
|
963
1064
|
}));
|
|
1065
|
+
let answer = null;
|
|
964
1066
|
try {
|
|
965
1067
|
const local = await openLocal();
|
|
966
|
-
await local.commit({
|
|
1068
|
+
answer = await local.commit({
|
|
967
1069
|
rooms: records.map(({ locator, record }) => ({
|
|
968
1070
|
locator,
|
|
969
1071
|
key: record.key,
|
|
970
1072
|
record: storedRecord(record),
|
|
971
|
-
|
|
1073
|
+
composed: composedOf(composed(record)),
|
|
972
1074
|
})),
|
|
973
1075
|
});
|
|
974
1076
|
}
|
|
975
1077
|
catch {
|
|
976
1078
|
journal.record('model revocation persistence failed');
|
|
977
1079
|
}
|
|
978
|
-
const keys = new Set(models.keys());
|
|
979
|
-
for (const locator of rooms.keys()) {
|
|
980
|
-
keys.add(modelKey(locator, 'collection'));
|
|
981
|
-
keys.add(modelKey(locator, 'typed-selection'));
|
|
982
|
-
}
|
|
1080
|
+
const keys = new Set([...models.keys(), ...rooms.keys()]);
|
|
983
1081
|
for (const key of keys) {
|
|
984
1082
|
invalidModels.add(key);
|
|
985
1083
|
models.set(key, revoked);
|
|
@@ -989,8 +1087,8 @@ export function composeRuntime(options) {
|
|
|
989
1087
|
for (const { locator, held, record } of records) {
|
|
990
1088
|
if (replicas.get(locator) !== held)
|
|
991
1089
|
continue;
|
|
992
|
-
|
|
993
|
-
replicas.set(locator, { ...held, record });
|
|
1090
|
+
const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
|
|
1091
|
+
replicas.set(locator, { ...held, record, indexed });
|
|
994
1092
|
}
|
|
995
1093
|
for (const locator of [...rooms.keys()]) {
|
|
996
1094
|
const room = rooms.get(locator);
|
|
@@ -1034,15 +1132,15 @@ export function composeRuntime(options) {
|
|
|
1034
1132
|
? null
|
|
1035
1133
|
: {
|
|
1036
1134
|
roots: selection.roots,
|
|
1037
|
-
model: modelTypes(
|
|
1135
|
+
model: modelTypes(builtIn),
|
|
1038
1136
|
},
|
|
1039
1137
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1040
1138
|
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
1041
1139
|
synchronizationAvailable,
|
|
1042
|
-
refusal: scopedRefusals.get(room.locator) ??
|
|
1140
|
+
refusal: scopedRefusals.get(room.locator) ?? null,
|
|
1043
1141
|
});
|
|
1044
1142
|
}
|
|
1045
|
-
async function applyDiscovery(found, started,
|
|
1143
|
+
async function applyDiscovery(found, started, left) {
|
|
1046
1144
|
const lockedBefore = lockedRooms();
|
|
1047
1145
|
for (const room of found) {
|
|
1048
1146
|
const fence = fences.get(room.locator);
|
|
@@ -1056,18 +1154,11 @@ export function composeRuntime(options) {
|
|
|
1056
1154
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
1057
1155
|
for (const locator of listedLocators)
|
|
1058
1156
|
scopedRefusals.delete(locator);
|
|
1059
|
-
if (
|
|
1157
|
+
if (left === undefined)
|
|
1060
1158
|
discovered.clear();
|
|
1061
|
-
else
|
|
1062
|
-
for (const locator of covered)
|
|
1063
|
-
discovered.delete(locator);
|
|
1064
1159
|
for (const room of listed)
|
|
1065
1160
|
discovered.set(room.locator, room);
|
|
1066
|
-
|
|
1067
|
-
if (options.adapter.exhaustiveDiscovery === true)
|
|
1068
|
-
await loseRooms(unlisted);
|
|
1069
|
-
else
|
|
1070
|
-
unlistedKept(unlisted);
|
|
1161
|
+
await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
|
|
1071
1162
|
for (const room of listed) {
|
|
1072
1163
|
const held = replicas.get(room.locator)?.record;
|
|
1073
1164
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -1098,15 +1189,6 @@ export function composeRuntime(options) {
|
|
|
1098
1189
|
]),
|
|
1099
1190
|
].filter((locator) => !listed.has(locator));
|
|
1100
1191
|
}
|
|
1101
|
-
function unlistedKept(unlisted) {
|
|
1102
|
-
for (const locator of unlisted) {
|
|
1103
|
-
const held = replicas.get(locator)?.record;
|
|
1104
|
-
if (held !== undefined)
|
|
1105
|
-
list(held, false);
|
|
1106
|
-
else
|
|
1107
|
-
rooms.delete(locator);
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
1192
|
function fence(locators) {
|
|
1111
1193
|
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1112
1194
|
const removed = fresh.map((locator) => ({
|
|
@@ -1120,28 +1202,24 @@ export function composeRuntime(options) {
|
|
|
1120
1202
|
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1121
1203
|
pendings.delete(pendingKey(locator, batch.id));
|
|
1122
1204
|
replicas.delete(locator);
|
|
1205
|
+
highest.delete(locator);
|
|
1206
|
+
beyondHorizon.delete(locator);
|
|
1123
1207
|
queuesWithoutReplica.delete(locator);
|
|
1124
1208
|
rooms.delete(locator);
|
|
1125
1209
|
roomDrafts.delete(locator);
|
|
1126
|
-
|
|
1210
|
+
attachedFiles.delete(locator);
|
|
1211
|
+
untransferred(locator);
|
|
1212
|
+
startedOver.delete(locator);
|
|
1127
1213
|
catching.delete(locator);
|
|
1128
1214
|
discovered.delete(locator);
|
|
1129
|
-
announced.delete(locator);
|
|
1130
1215
|
unreadable.delete(locator);
|
|
1131
1216
|
stale.delete(locator);
|
|
1132
1217
|
repairable.delete(locator);
|
|
1133
1218
|
unpreparable.delete(locator);
|
|
1134
|
-
paused.delete(locator);
|
|
1135
|
-
prepared.delete(locator);
|
|
1136
1219
|
scopedRefusals.delete(locator);
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
const key = modelKey(locator, attached);
|
|
1141
|
-
models.delete(key);
|
|
1142
|
-
liveModels.delete(key);
|
|
1143
|
-
invalidModels.delete(key);
|
|
1144
|
-
}
|
|
1220
|
+
models.delete(locator);
|
|
1221
|
+
liveModels.delete(locator);
|
|
1222
|
+
invalidModels.delete(locator);
|
|
1145
1223
|
journal.record(`room lost ${locator}`);
|
|
1146
1224
|
}
|
|
1147
1225
|
if (fresh.length === 0)
|
|
@@ -1189,7 +1267,14 @@ export function composeRuntime(options) {
|
|
|
1189
1267
|
});
|
|
1190
1268
|
await eraseFenced(fresh);
|
|
1191
1269
|
}
|
|
1192
|
-
function
|
|
1270
|
+
function retyped(persisted) {
|
|
1271
|
+
if (persisted.status === 'unavailable')
|
|
1272
|
+
return persisted;
|
|
1273
|
+
const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
|
|
1274
|
+
return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
|
|
1275
|
+
}
|
|
1276
|
+
function restored(key, held) {
|
|
1277
|
+
const persisted = retyped(held);
|
|
1193
1278
|
const prior = models.get(key);
|
|
1194
1279
|
if (prior !== undefined &&
|
|
1195
1280
|
prior.status !== 'unavailable' &&
|
|
@@ -1220,8 +1305,9 @@ export function composeRuntime(options) {
|
|
|
1220
1305
|
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1221
1306
|
targets = held.targets;
|
|
1222
1307
|
transfers.clear();
|
|
1223
|
-
|
|
1224
|
-
|
|
1308
|
+
arrivals.clear();
|
|
1309
|
+
streaming.clear();
|
|
1310
|
+
startedOver.clear();
|
|
1225
1311
|
limits = held.providerLimits;
|
|
1226
1312
|
capabilities = held.capabilities;
|
|
1227
1313
|
member = held.member;
|
|
@@ -1231,7 +1317,7 @@ export function composeRuntime(options) {
|
|
|
1231
1317
|
presentations.clear();
|
|
1232
1318
|
for (const record of held.presentation)
|
|
1233
1319
|
presentations.set(record.app, record.value);
|
|
1234
|
-
conversations.hydrate(heldConversations, chatDrafts);
|
|
1320
|
+
conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
|
|
1235
1321
|
pendings.clear();
|
|
1236
1322
|
for (const queue of queues) {
|
|
1237
1323
|
for (const { batch, state } of queue.pending) {
|
|
@@ -1239,7 +1325,7 @@ export function composeRuntime(options) {
|
|
|
1239
1325
|
}
|
|
1240
1326
|
}
|
|
1241
1327
|
replicas.clear();
|
|
1242
|
-
|
|
1328
|
+
reopened.clear();
|
|
1243
1329
|
queuesWithoutReplica.clear();
|
|
1244
1330
|
for (const [locator, queue] of queuedBatches) {
|
|
1245
1331
|
if (!held.rooms.has(locator))
|
|
@@ -1248,39 +1334,40 @@ export function composeRuntime(options) {
|
|
|
1248
1334
|
records.forEach((stored, index) => {
|
|
1249
1335
|
if (fences.has(stored.locator))
|
|
1250
1336
|
return;
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
const record = { ...stored, model, selectionModel };
|
|
1337
|
+
const record = { ...stored, model: restored(stored.locator, stored.model) };
|
|
1338
|
+
reopened.add(record.locator);
|
|
1254
1339
|
replicas.set(record.locator, {
|
|
1255
1340
|
record,
|
|
1256
1341
|
queue: queuedBatches.get(record.locator) ?? [],
|
|
1257
1342
|
overlaid: held.overlaid.get(record.key) ?? new Set(),
|
|
1258
1343
|
count: counts[index] ?? 0,
|
|
1259
1344
|
confirmed: 0,
|
|
1345
|
+
indexed: !held.unindexed.has(record.key),
|
|
1260
1346
|
});
|
|
1261
|
-
if (!held.unprepared.has(record.key))
|
|
1262
|
-
prepared.add(record.locator);
|
|
1263
1347
|
const existing = rooms.get(record.locator);
|
|
1264
1348
|
if (existing === undefined)
|
|
1265
|
-
list(record,
|
|
1349
|
+
list(record, record.roomVersion !== null);
|
|
1266
1350
|
else {
|
|
1267
1351
|
rooms.set(record.locator, {
|
|
1268
1352
|
...existing,
|
|
1269
1353
|
roomVersion: record.roomVersion,
|
|
1270
1354
|
operations: record.operations,
|
|
1271
1355
|
replica: record.replica,
|
|
1272
|
-
model: modelTypes(model),
|
|
1356
|
+
model: modelTypes(record.model),
|
|
1273
1357
|
typedSelection: record.typedSelection === null
|
|
1274
1358
|
? null
|
|
1275
|
-
: {
|
|
1276
|
-
roots: record.typedSelection.roots,
|
|
1277
|
-
model: modelTypes(selectionModel),
|
|
1278
|
-
},
|
|
1359
|
+
: { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
|
|
1279
1360
|
});
|
|
1280
1361
|
}
|
|
1281
1362
|
});
|
|
1282
|
-
|
|
1283
|
-
|
|
1363
|
+
try {
|
|
1364
|
+
for (const key of held.orphans)
|
|
1365
|
+
await opened.release(key);
|
|
1366
|
+
}
|
|
1367
|
+
catch (error) {
|
|
1368
|
+
if (!refusedForSpace(error))
|
|
1369
|
+
throw error;
|
|
1370
|
+
}
|
|
1284
1371
|
if (held.erasing || held.orphans.length > 0)
|
|
1285
1372
|
queueMicrotask(sweep);
|
|
1286
1373
|
return {
|
|
@@ -1305,7 +1392,7 @@ export function composeRuntime(options) {
|
|
|
1305
1392
|
async function open() {
|
|
1306
1393
|
if (closed)
|
|
1307
1394
|
throw new Error('runtime closed');
|
|
1308
|
-
const opened = openRuntimeStore(isFenced,
|
|
1395
|
+
const opened = openRuntimeStore(isFenced, database);
|
|
1309
1396
|
const interrupted = await opened.fences();
|
|
1310
1397
|
for (const locator of interrupted) {
|
|
1311
1398
|
fences.set(locator, { at: discoveries, erased: false });
|
|
@@ -1351,10 +1438,29 @@ export function composeRuntime(options) {
|
|
|
1351
1438
|
return opening;
|
|
1352
1439
|
}
|
|
1353
1440
|
function exclusive(work) {
|
|
1441
|
+
gathering = null;
|
|
1354
1442
|
const next = mutations.then(work, work);
|
|
1355
1443
|
mutations = next.then(() => undefined, () => undefined);
|
|
1356
1444
|
return next;
|
|
1357
1445
|
}
|
|
1446
|
+
function gathered(key, input, merge, work) {
|
|
1447
|
+
const tail = gathering;
|
|
1448
|
+
if (tail !== null && tail.key === key) {
|
|
1449
|
+
const merged = merge(tail.input, input);
|
|
1450
|
+
if (merged !== null) {
|
|
1451
|
+
tail.input = merged;
|
|
1452
|
+
return tail.done;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
const entry = { key, input, done: Promise.resolve() };
|
|
1456
|
+
entry.done = exclusive(() => {
|
|
1457
|
+
if (gathering === entry)
|
|
1458
|
+
gathering = null;
|
|
1459
|
+
return work(entry.input);
|
|
1460
|
+
});
|
|
1461
|
+
gathering = entry;
|
|
1462
|
+
return entry.done;
|
|
1463
|
+
}
|
|
1358
1464
|
function withdrawable(locator) {
|
|
1359
1465
|
return (batch) => {
|
|
1360
1466
|
const state = pendings.get(pendingKey(locator, batch.id));
|
|
@@ -1436,26 +1542,14 @@ export function composeRuntime(options) {
|
|
|
1436
1542
|
},
|
|
1437
1543
|
};
|
|
1438
1544
|
}
|
|
1439
|
-
function
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
fingerprint(compositionOf(before)) === fingerprint(compositionOf(after))) {
|
|
1443
|
-
return;
|
|
1444
|
-
}
|
|
1445
|
-
if (before !== undefined && before.key === after.key)
|
|
1446
|
-
prepared.delete(locator);
|
|
1447
|
-
}
|
|
1448
|
-
function committedRoom(locator, record, answer) {
|
|
1449
|
-
if (answer.prepared[locator] === false)
|
|
1450
|
-
prepared.delete(locator);
|
|
1451
|
-
else if (answer.prepared[locator] === true)
|
|
1452
|
-
prepared.add(locator);
|
|
1453
|
-
if (!prepared.has(locator))
|
|
1545
|
+
function committedRoom(locator, answer) {
|
|
1546
|
+
const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
|
|
1547
|
+
if (!indexed)
|
|
1454
1548
|
prepare([locator]);
|
|
1455
|
-
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
|
|
1549
|
+
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
|
|
1456
1550
|
}
|
|
1457
1551
|
function replayed(record, confirmedLayer, queue) {
|
|
1458
|
-
return stepped(replaying(confirmedLayer, queue, record.locator,
|
|
1552
|
+
return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
|
|
1459
1553
|
}
|
|
1460
1554
|
async function overlayDiff(record, before, visible, touched) {
|
|
1461
1555
|
const kept = before !== undefined && before.record.key === record.key
|
|
@@ -1475,8 +1569,6 @@ export function composeRuntime(options) {
|
|
|
1475
1569
|
function unpairedIn(locator, batch) {
|
|
1476
1570
|
if (!queueIn(locator).some((held) => held.id === batch.id))
|
|
1477
1571
|
return false;
|
|
1478
|
-
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
1479
|
-
return false;
|
|
1480
1572
|
return !leavingIn(batch, locator) || withdrawable(locator)(batch);
|
|
1481
1573
|
}
|
|
1482
1574
|
async function together(local, settlements) {
|
|
@@ -1518,7 +1610,7 @@ export function composeRuntime(options) {
|
|
|
1518
1610
|
const confirmedLayer = overrides === undefined
|
|
1519
1611
|
? read
|
|
1520
1612
|
: await stepped(slicingWith(read, overrides.set, overrides.removed));
|
|
1521
|
-
const composition =
|
|
1613
|
+
const composition = composed(record);
|
|
1522
1614
|
const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
|
|
1523
1615
|
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1524
1616
|
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
@@ -1561,6 +1653,7 @@ export function composeRuntime(options) {
|
|
|
1561
1653
|
key: record.key,
|
|
1562
1654
|
record: storedRecord(record),
|
|
1563
1655
|
roots: record.typedSelection?.roots ?? [],
|
|
1656
|
+
composed: composedOf(composition),
|
|
1564
1657
|
...settling.commit,
|
|
1565
1658
|
...(overrides === undefined
|
|
1566
1659
|
? {}
|
|
@@ -1602,14 +1695,14 @@ export function composeRuntime(options) {
|
|
|
1602
1695
|
const confirmedChanged = settling.commit?.published !== undefined ||
|
|
1603
1696
|
settling.commit?.refreshed === true ||
|
|
1604
1697
|
overrides !== undefined;
|
|
1605
|
-
|
|
1606
|
-
const { count } = committedRoom(locator, record, answer);
|
|
1698
|
+
const { count, indexed } = committedRoom(locator, answer);
|
|
1607
1699
|
replicas.set(locator, {
|
|
1608
1700
|
record,
|
|
1609
1701
|
queue: kept,
|
|
1610
1702
|
overlaid: touched,
|
|
1611
1703
|
count,
|
|
1612
1704
|
confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
|
|
1705
|
+
indexed,
|
|
1613
1706
|
});
|
|
1614
1707
|
return mergeChangeSets([
|
|
1615
1708
|
changeSet(locator, {
|
|
@@ -1624,110 +1717,105 @@ export function composeRuntime(options) {
|
|
|
1624
1717
|
};
|
|
1625
1718
|
}
|
|
1626
1719
|
function adoptModels(locator, record) {
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
if (model.status === 'current')
|
|
1635
|
-
liveModels.set(key, JSON.stringify(model.snapshot));
|
|
1636
|
-
else if (model.status === 'unavailable')
|
|
1637
|
-
liveModels.delete(key);
|
|
1638
|
-
}
|
|
1720
|
+
const model = record.model;
|
|
1721
|
+
models.set(locator, model);
|
|
1722
|
+
invalidModels.delete(locator);
|
|
1723
|
+
if (model.status === 'current')
|
|
1724
|
+
liveModels.set(locator, JSON.stringify(model.snapshot));
|
|
1725
|
+
else if (model.status === 'unavailable')
|
|
1726
|
+
liveModels.delete(locator);
|
|
1639
1727
|
}
|
|
1640
1728
|
function remodel(locator, answers, held) {
|
|
1641
|
-
const
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
};
|
|
1652
|
-
return {
|
|
1653
|
-
collection: taken(answers?.collection, held.collection, 'collection'),
|
|
1654
|
-
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
1655
|
-
};
|
|
1729
|
+
const answer = answers?.collection;
|
|
1730
|
+
if (answer === undefined)
|
|
1731
|
+
return held;
|
|
1732
|
+
const { state, refused } = attachedModel(answer, held.collection, roomTypes);
|
|
1733
|
+
if (refused !== null) {
|
|
1734
|
+
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
1735
|
+
journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
|
|
1736
|
+
}
|
|
1737
|
+
models.set(locator, state);
|
|
1738
|
+
return { collection: state };
|
|
1656
1739
|
}
|
|
1657
|
-
function publish(local, room,
|
|
1658
|
-
|
|
1740
|
+
function publish(local, room, reached, landing, live) {
|
|
1741
|
+
const landed = exclusive(async () => {
|
|
1659
1742
|
if (!live()) {
|
|
1660
1743
|
caughtUpTo(room.locator);
|
|
1661
1744
|
return true;
|
|
1662
1745
|
}
|
|
1663
1746
|
const prior = replicas.get(room.locator);
|
|
1664
|
-
if (landing.
|
|
1665
|
-
(
|
|
1666
|
-
|
|
1747
|
+
if (!stands(room.locator, landing.from)) {
|
|
1748
|
+
if (landing.kind === 'refreshed') {
|
|
1749
|
+
await local.unrefresh(reached.key);
|
|
1750
|
+
return false;
|
|
1751
|
+
}
|
|
1752
|
+
caughtUpTo(room.locator);
|
|
1753
|
+
await abandoned(local, room.locator);
|
|
1667
1754
|
return false;
|
|
1668
1755
|
}
|
|
1756
|
+
const record = landing.kind === 'refreshed' && prior !== undefined
|
|
1757
|
+
? {
|
|
1758
|
+
...prior.record,
|
|
1759
|
+
roomVersion: reached.roomVersion,
|
|
1760
|
+
...(landing.remodelled ? { model: reached.model } : {}),
|
|
1761
|
+
}
|
|
1762
|
+
: reached;
|
|
1669
1763
|
adoptModels(room.locator, record);
|
|
1670
1764
|
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
1671
|
-
const roomVersion = record.roomVersion;
|
|
1672
|
-
const accepted = queue.filter((batch) => {
|
|
1673
|
-
const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
|
|
1674
|
-
return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
|
|
1675
|
-
});
|
|
1676
1765
|
let set;
|
|
1677
1766
|
try {
|
|
1678
|
-
set = await settle(local, room.locator, prior, record, queue
|
|
1767
|
+
set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
|
|
1679
1768
|
? {
|
|
1680
1769
|
room: true,
|
|
1681
1770
|
view: 'refreshed',
|
|
1682
|
-
commit: { refreshed: true
|
|
1771
|
+
commit: { refreshed: true },
|
|
1683
1772
|
}
|
|
1684
1773
|
: {
|
|
1685
1774
|
room: true,
|
|
1686
1775
|
view: landing.refreshed ? 'refreshed' : 'confirmed',
|
|
1687
1776
|
commit: {
|
|
1688
1777
|
published: { from: prior?.record.key ?? null },
|
|
1689
|
-
composition: compositionOf(record),
|
|
1690
1778
|
...(landing.refreshed ? { refreshed: true } : {}),
|
|
1691
1779
|
},
|
|
1692
1780
|
});
|
|
1693
1781
|
}
|
|
1694
1782
|
catch (error) {
|
|
1695
|
-
if (!
|
|
1783
|
+
if (!refusedForSpace(error))
|
|
1696
1784
|
throw error;
|
|
1697
|
-
await exceededOn(room.locator, null);
|
|
1698
1785
|
caughtUpTo(room.locator);
|
|
1699
1786
|
return true;
|
|
1700
1787
|
}
|
|
1701
1788
|
if (landing.kind === 'bootstrapped') {
|
|
1702
|
-
|
|
1789
|
+
highest.delete(room.locator);
|
|
1790
|
+
beyondHorizon.delete(room.locator);
|
|
1791
|
+
untransferred(room.locator);
|
|
1792
|
+
startedOver.delete(room.locator);
|
|
1703
1793
|
catching.delete(room.locator);
|
|
1704
|
-
|
|
1794
|
+
if (prior !== undefined && prior.record.key !== record.key)
|
|
1795
|
+
sweep();
|
|
1796
|
+
name(room.locator);
|
|
1705
1797
|
}
|
|
1706
|
-
for (const batch of accepted)
|
|
1707
|
-
pendings.delete(pendingKey(room.locator, batch.id));
|
|
1708
1798
|
stale.delete(room.locator);
|
|
1709
1799
|
unpreparable.delete(room.locator);
|
|
1710
1800
|
if (unreadable.delete(room.locator))
|
|
1711
1801
|
scopedRefusals.delete(room.locator);
|
|
1712
|
-
sourceRefusals.delete(room.locator);
|
|
1713
|
-
declarationRefusals.delete(room.locator);
|
|
1714
1802
|
list(room, true);
|
|
1715
1803
|
publishChangeSet(set);
|
|
1716
1804
|
await unpair(local);
|
|
1717
1805
|
moveTo({ ...status, ...attention() });
|
|
1718
|
-
|
|
1719
|
-
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1806
|
+
if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1720
1807
|
submitLater(room.locator);
|
|
1721
1808
|
}
|
|
1722
1809
|
return true;
|
|
1723
1810
|
});
|
|
1811
|
+
return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
|
|
1724
1812
|
}
|
|
1725
1813
|
async function savedRoom(locator) {
|
|
1726
1814
|
const local = await openLocal();
|
|
1727
1815
|
const replica = replicas.get(locator);
|
|
1728
1816
|
const room = rooms.get(locator);
|
|
1729
1817
|
if (replica === undefined || room === undefined) {
|
|
1730
|
-
throw runtimeError('room
|
|
1818
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
1731
1819
|
}
|
|
1732
1820
|
return { local, replica, room };
|
|
1733
1821
|
}
|
|
@@ -1745,12 +1833,7 @@ export function composeRuntime(options) {
|
|
|
1745
1833
|
return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
|
|
1746
1834
|
}
|
|
1747
1835
|
async function saveParts(locator, named, writtenBy) {
|
|
1748
|
-
const read = named
|
|
1749
|
-
if (read.some(({ path }, at) => path === '' ||
|
|
1750
|
-
normalize(path) !== path ||
|
|
1751
|
-
read.findIndex((one) => one.path === path) !== at)) {
|
|
1752
|
-
throw runtimeError('room-draft-malformed');
|
|
1753
|
-
}
|
|
1836
|
+
const read = readSavedParts(named);
|
|
1754
1837
|
const target = await savedRoom(locator);
|
|
1755
1838
|
const saved = [];
|
|
1756
1839
|
const files = [];
|
|
@@ -1759,7 +1842,7 @@ export function composeRuntime(options) {
|
|
|
1759
1842
|
for (const { path, parts } of read) {
|
|
1760
1843
|
const file = parts.file === undefined
|
|
1761
1844
|
? undefined
|
|
1762
|
-
: await heldFile(parts.file.blob, binaryBase(visible, path));
|
|
1845
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
|
|
1763
1846
|
if (parts.file !== undefined && file !== undefined) {
|
|
1764
1847
|
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1765
1848
|
}
|
|
@@ -1794,13 +1877,14 @@ export function composeRuntime(options) {
|
|
|
1794
1877
|
const referring = removed.length === 0
|
|
1795
1878
|
? []
|
|
1796
1879
|
: (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
|
|
1797
|
-
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
|
|
1880
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
|
|
1798
1881
|
const { visible } = await replayed(record, confirmedLayer, replica.queue);
|
|
1799
|
-
const composition =
|
|
1882
|
+
const composition = composed(record);
|
|
1800
1883
|
return {
|
|
1801
1884
|
record,
|
|
1802
1885
|
confirmed: confirmedLayer,
|
|
1803
1886
|
visible,
|
|
1887
|
+
folders: new Set(await local.folders(record.key, 'visible', paths)),
|
|
1804
1888
|
*visibleOver(queue) {
|
|
1805
1889
|
return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
|
|
1806
1890
|
},
|
|
@@ -1812,13 +1896,7 @@ export function composeRuntime(options) {
|
|
|
1812
1896
|
return false;
|
|
1813
1897
|
const local = await openLocal();
|
|
1814
1898
|
const roots = record.typedSelection?.roots ?? [];
|
|
1815
|
-
|
|
1816
|
-
(await local.typedWithin(record.key, 'visible', roots, false))) {
|
|
1817
|
-
return true;
|
|
1818
|
-
}
|
|
1819
|
-
return (composition.selection !== null &&
|
|
1820
|
-
composition.selection.model.status === 'unavailable' &&
|
|
1821
|
-
(await local.typedWithin(record.key, 'visible', roots, true)));
|
|
1899
|
+
return local.typedWithin(record.key, 'visible', roots, false);
|
|
1822
1900
|
}
|
|
1823
1901
|
function textDiffers(left, right) {
|
|
1824
1902
|
return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
|
|
@@ -1829,18 +1907,6 @@ export function composeRuntime(options) {
|
|
|
1829
1907
|
return undefined;
|
|
1830
1908
|
return entry.binary !== null ? null : entry.content;
|
|
1831
1909
|
}
|
|
1832
|
-
function walkOf(local, key, view) {
|
|
1833
|
-
return async (after) => {
|
|
1834
|
-
const page = await local.walk(key, view, after, WALK_BYTES);
|
|
1835
|
-
return {
|
|
1836
|
-
entries: page.entries.map(({ path, content, binary }) => ({
|
|
1837
|
-
path,
|
|
1838
|
-
content: binary !== null ? null : content,
|
|
1839
|
-
})),
|
|
1840
|
-
next: page.next,
|
|
1841
|
-
};
|
|
1842
|
-
};
|
|
1843
|
-
}
|
|
1844
1910
|
function lookupOf(local, key, view) {
|
|
1845
1911
|
return async (paths) => {
|
|
1846
1912
|
if (paths.length === 0)
|
|
@@ -1857,37 +1923,6 @@ export function composeRuntime(options) {
|
|
|
1857
1923
|
const read = parseFrontmatter(entry.content ?? '');
|
|
1858
1924
|
return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
|
|
1859
1925
|
}
|
|
1860
|
-
function overriddenWalk(walk, overrides) {
|
|
1861
|
-
const sorted = [...overrides.keys()].sort();
|
|
1862
|
-
const beyond = (path) => {
|
|
1863
|
-
if (path === null)
|
|
1864
|
-
return 0;
|
|
1865
|
-
let low = 0;
|
|
1866
|
-
let high = sorted.length;
|
|
1867
|
-
while (low < high) {
|
|
1868
|
-
const middle = (low + high) >>> 1;
|
|
1869
|
-
if (sorted[middle] <= path)
|
|
1870
|
-
low = middle + 1;
|
|
1871
|
-
else
|
|
1872
|
-
high = middle;
|
|
1873
|
-
}
|
|
1874
|
-
return low;
|
|
1875
|
-
};
|
|
1876
|
-
return async (after) => {
|
|
1877
|
-
const page = await walk(after);
|
|
1878
|
-
const end = page.next;
|
|
1879
|
-
const added = sorted
|
|
1880
|
-
.slice(beyond(after), end === null ? sorted.length : beyond(end))
|
|
1881
|
-
.flatMap((path) => {
|
|
1882
|
-
const entry = overrides.get(path);
|
|
1883
|
-
return entry === null || entry === undefined
|
|
1884
|
-
? []
|
|
1885
|
-
: [{ path, content: entry.binary !== null ? null : entry.content }];
|
|
1886
|
-
});
|
|
1887
|
-
const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
1888
|
-
return { entries, next: end };
|
|
1889
|
-
};
|
|
1890
|
-
}
|
|
1891
1926
|
function overriddenLookup(lookup, overrides) {
|
|
1892
1927
|
return async (paths) => {
|
|
1893
1928
|
const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
|
|
@@ -1905,9 +1940,8 @@ export function composeRuntime(options) {
|
|
|
1905
1940
|
}
|
|
1906
1941
|
function validatorOf(record, confirmedLayer) {
|
|
1907
1942
|
return async (resulting, touched) => {
|
|
1908
|
-
const composition = compositionFor(record);
|
|
1909
1943
|
const local = await openLocal();
|
|
1910
|
-
const
|
|
1944
|
+
const changes = [];
|
|
1911
1945
|
const overrides = new Map();
|
|
1912
1946
|
const pace = paced();
|
|
1913
1947
|
for (const path of new Set([...confirmedLayer.held, ...touched])) {
|
|
@@ -1918,19 +1952,13 @@ export function composeRuntime(options) {
|
|
|
1918
1952
|
continue;
|
|
1919
1953
|
overrides.set(path, now);
|
|
1920
1954
|
if (textOf(was) !== textOf(now))
|
|
1921
|
-
|
|
1955
|
+
changes.push({ path, was: textOf(was), now: textOf(now) });
|
|
1922
1956
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
texts: textsOf(local, record.key, 'confirmed'),
|
|
1929
|
-
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
1930
|
-
}));
|
|
1931
|
-
if (clean)
|
|
1932
|
-
return { verdict: 'accept' };
|
|
1933
|
-
return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
|
|
1957
|
+
return validatedNeighbourhood(changes, compositionFor(record), {
|
|
1958
|
+
lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
|
|
1959
|
+
texts: textsOf(local, record.key, 'confirmed'),
|
|
1960
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
1961
|
+
});
|
|
1934
1962
|
};
|
|
1935
1963
|
}
|
|
1936
1964
|
function textsOf(local, key, view) {
|
|
@@ -1954,6 +1982,7 @@ export function composeRuntime(options) {
|
|
|
1954
1982
|
operations: room.operations,
|
|
1955
1983
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1956
1984
|
visible: saving.visible,
|
|
1985
|
+
folders: saving.folders,
|
|
1957
1986
|
queued: queuedDocuments(replica.queue),
|
|
1958
1987
|
unmodeled: await unmodeledIn(record),
|
|
1959
1988
|
validate: validatorOf(record, saving.confirmed),
|
|
@@ -1974,6 +2003,7 @@ export function composeRuntime(options) {
|
|
|
1974
2003
|
locator,
|
|
1975
2004
|
key: saving.record.key,
|
|
1976
2005
|
roots: saving.record.typedSelection?.roots ?? [],
|
|
2006
|
+
composed: composedOf(composed(saving.record)),
|
|
1977
2007
|
overlay: diff.overlay,
|
|
1978
2008
|
});
|
|
1979
2009
|
}
|
|
@@ -2005,13 +2035,13 @@ export function composeRuntime(options) {
|
|
|
2005
2035
|
}
|
|
2006
2036
|
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
2007
2037
|
const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
|
|
2008
|
-
const changed = saved.flatMap(({ path, parts }) => changedParts(path,
|
|
2038
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
|
|
2009
2039
|
if (changed.length > 0)
|
|
2010
2040
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2011
2041
|
const target = replicas.get(destination);
|
|
2012
2042
|
const listedRoom = rooms.get(destination);
|
|
2013
2043
|
if (target === undefined || listedRoom === undefined) {
|
|
2014
|
-
throw runtimeError('room
|
|
2044
|
+
throw runtimeError('unknown-room', `Unknown Room ${destination}`);
|
|
2015
2045
|
}
|
|
2016
2046
|
const arriving = await savingIn(target, [], moves.map(({ to }) => to));
|
|
2017
2047
|
const built = await buildMove({
|
|
@@ -2074,10 +2104,10 @@ export function composeRuntime(options) {
|
|
|
2074
2104
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2075
2105
|
const { answer, touched } = committedBoth;
|
|
2076
2106
|
if (answer.fenced !== null) {
|
|
2077
|
-
throw runtimeError('room
|
|
2107
|
+
throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
|
|
2078
2108
|
}
|
|
2079
2109
|
if (fences.has(locator) || fences.has(destination)) {
|
|
2080
|
-
throw runtimeError('room
|
|
2110
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2081
2111
|
}
|
|
2082
2112
|
pendings.set(pendingKey(locator, id), {
|
|
2083
2113
|
drafts: saved,
|
|
@@ -2117,6 +2147,8 @@ export function composeRuntime(options) {
|
|
|
2117
2147
|
submitLater(locator);
|
|
2118
2148
|
}
|
|
2119
2149
|
async function committed(locator, target, saved, consumed, writtenBy, files = []) {
|
|
2150
|
+
if (full)
|
|
2151
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2120
2152
|
const into = movedInto(locator, saved);
|
|
2121
2153
|
if (into !== null) {
|
|
2122
2154
|
return committedMove(locator, into, target, saved, consumed, writtenBy, files);
|
|
@@ -2128,7 +2160,7 @@ export function composeRuntime(options) {
|
|
|
2128
2160
|
...saved.map(({ path }) => path),
|
|
2129
2161
|
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
2130
2162
|
]);
|
|
2131
|
-
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path,
|
|
2163
|
+
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
|
|
2132
2164
|
if (changed.length > 0)
|
|
2133
2165
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2134
2166
|
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
|
|
@@ -2156,6 +2188,7 @@ export function composeRuntime(options) {
|
|
|
2156
2188
|
providerLimits: limits,
|
|
2157
2189
|
handle: room.handle,
|
|
2158
2190
|
visible: saving.visible,
|
|
2191
|
+
folders: saving.folders,
|
|
2159
2192
|
changes,
|
|
2160
2193
|
bases: parts.flatMap(({ bases }) => bases),
|
|
2161
2194
|
queued: queuedDocuments(replica.queue),
|
|
@@ -2245,10 +2278,10 @@ export function composeRuntime(options) {
|
|
|
2245
2278
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2246
2279
|
const { answer, touched } = done;
|
|
2247
2280
|
if (answer.fenced !== null) {
|
|
2248
|
-
throw runtimeError('room
|
|
2281
|
+
throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
|
|
2249
2282
|
}
|
|
2250
2283
|
if (fences.has(locator))
|
|
2251
|
-
throw runtimeError('room
|
|
2284
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2252
2285
|
for (const origin of withdrawn.keys())
|
|
2253
2286
|
pendings.delete(pendingKey(locator, origin));
|
|
2254
2287
|
for (const [id, drafts] of drafted) {
|
|
@@ -2297,7 +2330,7 @@ export function composeRuntime(options) {
|
|
|
2297
2330
|
throw new Error('connection refused');
|
|
2298
2331
|
await local.keepUpload(locator, identifier, upload);
|
|
2299
2332
|
if (fences.has(locator))
|
|
2300
|
-
throw runtimeError('room
|
|
2333
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2301
2334
|
const key = pendingKey(locator, identifier);
|
|
2302
2335
|
pendings.set(key, { ...pendings.get(key), upload });
|
|
2303
2336
|
if (!continuing())
|
|
@@ -2306,6 +2339,9 @@ export function composeRuntime(options) {
|
|
|
2306
2339
|
async function readHistory(locator, path, query = {}) {
|
|
2307
2340
|
if (closed || refusal !== null)
|
|
2308
2341
|
return UNCERTAIN;
|
|
2342
|
+
await firstDiscovery;
|
|
2343
|
+
if (!roomsNamed)
|
|
2344
|
+
return UNCERTAIN;
|
|
2309
2345
|
try {
|
|
2310
2346
|
await ensureAuthorization();
|
|
2311
2347
|
}
|
|
@@ -2395,14 +2431,12 @@ export function composeRuntime(options) {
|
|
|
2395
2431
|
return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
|
|
2396
2432
|
};
|
|
2397
2433
|
}
|
|
2398
|
-
function binaryRoom(request) {
|
|
2399
|
-
if (request.kind === 'path')
|
|
2400
|
-
return request.locator;
|
|
2401
|
-
return options.adapter.referrerScope(request.referrer)?.locator ?? null;
|
|
2402
|
-
}
|
|
2403
2434
|
async function transferBinary(request, transfer, answer) {
|
|
2404
2435
|
if (closed || refusal !== null)
|
|
2405
2436
|
return UNCERTAIN;
|
|
2437
|
+
await firstDiscovery;
|
|
2438
|
+
if (!roomsNamed)
|
|
2439
|
+
return UNCERTAIN;
|
|
2406
2440
|
try {
|
|
2407
2441
|
await ensureAuthorization();
|
|
2408
2442
|
}
|
|
@@ -2412,16 +2446,13 @@ export function composeRuntime(options) {
|
|
|
2412
2446
|
if (closed || authorization === undefined || refusal !== null)
|
|
2413
2447
|
return UNCERTAIN;
|
|
2414
2448
|
const since = fencings;
|
|
2415
|
-
journal.record(request.
|
|
2416
|
-
? `transfer binary ${request.locator} ${request.path}`
|
|
2417
|
-
: `transfer binary ${request.fileRef}`);
|
|
2449
|
+
journal.record(`transfer binary ${request.locator} ${request.path}`);
|
|
2418
2450
|
try {
|
|
2419
2451
|
const result = await transfer(authorization);
|
|
2420
2452
|
const settled = result;
|
|
2421
2453
|
journal.record(`outcome binary ${settled.kind}`);
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
return failure({ kind: 'room-not-readable', locator });
|
|
2454
|
+
if (fencedSince(request.locator, since))
|
|
2455
|
+
return failure({ kind: 'room-not-readable', locator: request.locator });
|
|
2425
2456
|
if (settled.kind === 'scoped-refusal') {
|
|
2426
2457
|
const room = replicas.get(settled.locator)?.record;
|
|
2427
2458
|
if (room !== undefined)
|
|
@@ -2436,23 +2467,27 @@ export function composeRuntime(options) {
|
|
|
2436
2467
|
return UNCERTAIN;
|
|
2437
2468
|
}
|
|
2438
2469
|
}
|
|
2470
|
+
function fetchBinary(request, name) {
|
|
2471
|
+
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
2472
|
+
}
|
|
2439
2473
|
async function readBinary(request, name) {
|
|
2440
2474
|
if (closed)
|
|
2441
2475
|
return UNCERTAIN;
|
|
2442
2476
|
const held = await keptFiles.find(name);
|
|
2443
2477
|
if (held !== null)
|
|
2444
2478
|
return held;
|
|
2445
|
-
return
|
|
2479
|
+
return fetchBinary(request, name);
|
|
2446
2480
|
}
|
|
2447
2481
|
async function binaryPath(locator, path, revision) {
|
|
2448
2482
|
if (revision !== undefined)
|
|
2449
2483
|
return { kind: 'path', locator, path, revision };
|
|
2450
2484
|
const replica = replicas.get(locator);
|
|
2451
|
-
if (replica === undefined)
|
|
2452
|
-
return null;
|
|
2453
2485
|
const local = await openLocal();
|
|
2454
|
-
const [held] = await local.entries(replica.record.key, 'visible', [path]);
|
|
2455
|
-
|
|
2486
|
+
const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
|
|
2487
|
+
const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
|
|
2488
|
+
if (attached !== undefined)
|
|
2489
|
+
return { kind: 'path', locator, path, revision: attached.revision };
|
|
2490
|
+
if (replica === undefined || held?.binary === undefined || held.binary === null)
|
|
2456
2491
|
return null;
|
|
2457
2492
|
if (held.binary.state === 'pending')
|
|
2458
2493
|
return { kind: 'pending', localId: held.binary.localId };
|
|
@@ -2468,7 +2503,7 @@ export function composeRuntime(options) {
|
|
|
2468
2503
|
const holds = (at) => {
|
|
2469
2504
|
const kept = confirmedLayer.binaries[at];
|
|
2470
2505
|
return (kept?.state === 'accepted' &&
|
|
2471
|
-
kept.
|
|
2506
|
+
kept.sha256 === record.sha256 &&
|
|
2472
2507
|
confirmedLayer.revisions[at] === record.revision);
|
|
2473
2508
|
};
|
|
2474
2509
|
let confirmedAt = path;
|
|
@@ -2482,58 +2517,93 @@ export function composeRuntime(options) {
|
|
|
2482
2517
|
locator,
|
|
2483
2518
|
path: confirmedAt,
|
|
2484
2519
|
revision: record.revision,
|
|
2485
|
-
fileRef: record.fileRef,
|
|
2486
2520
|
};
|
|
2487
2521
|
}
|
|
2488
|
-
async function
|
|
2522
|
+
async function askedBinary(locator, path, revision) {
|
|
2489
2523
|
await ready;
|
|
2490
2524
|
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2491
|
-
|
|
2525
|
+
return drafted !== undefined && drafted.localId === revision
|
|
2492
2526
|
? { kind: 'pending', localId: revision }
|
|
2493
|
-
:
|
|
2527
|
+
: binaryPath(locator, path, revision);
|
|
2528
|
+
}
|
|
2529
|
+
function pendingBinary(locator, path, localId) {
|
|
2530
|
+
const facts = queuedFacts(locator, path, localId);
|
|
2531
|
+
return facts === null
|
|
2532
|
+
? absentBinary(path)
|
|
2533
|
+
: {
|
|
2534
|
+
kind: 'binary',
|
|
2535
|
+
file: localId,
|
|
2536
|
+
byteLength: facts.byteLength,
|
|
2537
|
+
mediaType: facts.mediaType,
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
async function readBinaryPath(locator, path, revision) {
|
|
2541
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2494
2542
|
if (asked === null)
|
|
2495
2543
|
return absentBinary(path);
|
|
2496
|
-
if (asked.kind === 'pending')
|
|
2497
|
-
|
|
2498
|
-
return facts === null
|
|
2499
|
-
? absentBinary(path)
|
|
2500
|
-
: {
|
|
2501
|
-
kind: 'binary',
|
|
2502
|
-
file: asked.localId,
|
|
2503
|
-
byteLength: facts.byteLength,
|
|
2504
|
-
mediaType: facts.mediaType,
|
|
2505
|
-
};
|
|
2506
|
-
}
|
|
2544
|
+
if (asked.kind === 'pending')
|
|
2545
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2507
2546
|
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
2508
2547
|
}
|
|
2548
|
+
async function heldMediaType(locator, path) {
|
|
2549
|
+
const replica = replicas.get(locator);
|
|
2550
|
+
const [held] = replica === undefined
|
|
2551
|
+
? []
|
|
2552
|
+
: await (await openLocal()).entries(replica.record.key, 'visible', [path]);
|
|
2553
|
+
return held === undefined
|
|
2554
|
+
? attachedFiles.get(locator)?.get(path)?.mediaType
|
|
2555
|
+
: held.binary?.mediaType;
|
|
2556
|
+
}
|
|
2557
|
+
function linkOf(asked, held) {
|
|
2558
|
+
return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
|
|
2559
|
+
}
|
|
2509
2560
|
async function linkBinaryPath(locator, path, revision) {
|
|
2510
2561
|
await ready;
|
|
2511
2562
|
const asked = await binaryPath(locator, path, revision);
|
|
2512
2563
|
if (asked === null || asked.kind === 'pending')
|
|
2513
2564
|
return absentBinary(path);
|
|
2514
|
-
return
|
|
2565
|
+
return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
|
|
2515
2566
|
}
|
|
2516
|
-
async function
|
|
2567
|
+
async function showBinaryPath(locator, path, revision) {
|
|
2568
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2569
|
+
if (asked === null)
|
|
2570
|
+
return absentBinary(path);
|
|
2571
|
+
if (asked.kind === 'pending')
|
|
2572
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2573
|
+
if (closed)
|
|
2574
|
+
return UNCERTAIN;
|
|
2575
|
+
const name = { locator, path: asked.path, revision: asked.revision };
|
|
2576
|
+
const kept = await keptFiles.find(name);
|
|
2577
|
+
if (kept !== null)
|
|
2578
|
+
return kept;
|
|
2579
|
+
const mediaType = await heldMediaType(locator, path);
|
|
2580
|
+
if (mediaType?.startsWith('image/') === true)
|
|
2581
|
+
return fetchBinary(asked, name);
|
|
2582
|
+
return linkOf(asked, revision === undefined ? mediaType : undefined);
|
|
2583
|
+
}
|
|
2584
|
+
async function attachBinary(locator, file) {
|
|
2517
2585
|
await ready;
|
|
2518
|
-
|
|
2519
|
-
if (
|
|
2520
|
-
return
|
|
2521
|
-
|
|
2586
|
+
await firstDiscovery;
|
|
2587
|
+
if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
|
|
2588
|
+
return OFFLINE;
|
|
2589
|
+
const request = await attachedFile(locator, file);
|
|
2590
|
+
return transferBinary({ locator, path: file.name }, async (held) => {
|
|
2591
|
+
const result = await options.adapter.attach(held, request);
|
|
2592
|
+
if (result.kind === 'attached') {
|
|
2593
|
+
const landed = attachedFiles.get(locator) ?? new Map();
|
|
2594
|
+
landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
|
|
2595
|
+
attachedFiles.set(locator, landed);
|
|
2596
|
+
}
|
|
2597
|
+
return result;
|
|
2598
|
+
}, attachAnswer);
|
|
2522
2599
|
}
|
|
2523
2600
|
function readPart(file, part) {
|
|
2524
|
-
return
|
|
2601
|
+
return database.ask({ kind: 'file.part', key: file, part });
|
|
2525
2602
|
}
|
|
2526
2603
|
async function cleanUp() {
|
|
2527
|
-
const freed = await
|
|
2604
|
+
const freed = await database.ask({ kind: 'opened.clean' });
|
|
2528
2605
|
return freed + ((await options.shell.cleanUp?.()) ?? 0);
|
|
2529
2606
|
}
|
|
2530
|
-
function declaredAgents() {
|
|
2531
|
-
for (const capability of capabilities) {
|
|
2532
|
-
if (capability.conversationAgents !== undefined)
|
|
2533
|
-
return capability.conversationAgents;
|
|
2534
|
-
}
|
|
2535
|
-
return undefined;
|
|
2536
|
-
}
|
|
2537
2607
|
function declaredAction(name) {
|
|
2538
2608
|
for (const capability of capabilities) {
|
|
2539
2609
|
for (const action of capability.actions ?? []) {
|
|
@@ -2585,6 +2655,24 @@ export function composeRuntime(options) {
|
|
|
2585
2655
|
return UNCERTAIN;
|
|
2586
2656
|
}
|
|
2587
2657
|
}
|
|
2658
|
+
async function readConversationHistory(request) {
|
|
2659
|
+
await ready;
|
|
2660
|
+
const stream = options.adapter.conversations;
|
|
2661
|
+
if (closed || refusal !== null || stream?.history === undefined)
|
|
2662
|
+
return UNCERTAIN;
|
|
2663
|
+
journal.record('transfer conversation history');
|
|
2664
|
+
try {
|
|
2665
|
+
const result = await stream.history(request);
|
|
2666
|
+
if (result.kind === 'revoked-authorization')
|
|
2667
|
+
await refuse('revoked-authorization');
|
|
2668
|
+
journal.record(`outcome conversation history ${result.kind}`);
|
|
2669
|
+
return calledResult(result);
|
|
2670
|
+
}
|
|
2671
|
+
catch {
|
|
2672
|
+
journal.record('outcome conversation history transport-uncertainty');
|
|
2673
|
+
return UNCERTAIN;
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2588
2676
|
function unsettled(result) {
|
|
2589
2677
|
return result.kind !== 'action' && result.retryable;
|
|
2590
2678
|
}
|
|
@@ -2646,6 +2734,33 @@ export function composeRuntime(options) {
|
|
|
2646
2734
|
const held = replicas.get(locator);
|
|
2647
2735
|
return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
|
|
2648
2736
|
}
|
|
2737
|
+
function shownAt(locator) {
|
|
2738
|
+
const held = replicas.get(locator)?.record.roomVersion;
|
|
2739
|
+
const raised = highest.get(locator);
|
|
2740
|
+
if (held === undefined || raised === undefined)
|
|
2741
|
+
return held ?? raised;
|
|
2742
|
+
return options.adapter.reached(locator, held, raised) ? held : raised;
|
|
2743
|
+
}
|
|
2744
|
+
function showing(locator, roomVersion) {
|
|
2745
|
+
const at = shownAt(locator);
|
|
2746
|
+
if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
|
|
2747
|
+
highest.set(locator, roomVersion);
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
function older(locator, roomVersion) {
|
|
2751
|
+
if (beyondHorizon.has(locator))
|
|
2752
|
+
return false;
|
|
2753
|
+
const at = shownAt(locator);
|
|
2754
|
+
return (at !== undefined &&
|
|
2755
|
+
options.adapter.reached(locator, at, roomVersion) &&
|
|
2756
|
+
!options.adapter.reached(locator, roomVersion, at));
|
|
2757
|
+
}
|
|
2758
|
+
function replaced(locators) {
|
|
2759
|
+
for (const locator of locators) {
|
|
2760
|
+
highest.delete(locator);
|
|
2761
|
+
beyondHorizon.add(locator);
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2649
2764
|
async function writtenAfter(locator, roomVersion, path) {
|
|
2650
2765
|
const held = replicas.get(locator);
|
|
2651
2766
|
if (held === undefined)
|
|
@@ -2656,169 +2771,233 @@ export function composeRuntime(options) {
|
|
|
2656
2771
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
2657
2772
|
!options.adapter.reached(locator, roomVersion, written));
|
|
2658
2773
|
}
|
|
2659
|
-
function
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
else
|
|
2669
|
-
known.set(path, entry.binary !== null ? null : entry.content);
|
|
2670
|
-
}
|
|
2671
|
-
return known;
|
|
2672
|
-
};
|
|
2673
|
-
}
|
|
2674
|
-
async function unreadableRoom(texts, walk, paths) {
|
|
2675
|
-
if (paths !== null) {
|
|
2676
|
-
const text = {};
|
|
2677
|
-
for (const [path, content] of await texts(paths))
|
|
2678
|
-
if (content !== null)
|
|
2679
|
-
text[path] = content;
|
|
2680
|
-
return stepped(unreadableIn(text, Object.keys(text)));
|
|
2681
|
-
}
|
|
2682
|
-
for (let after = null;;) {
|
|
2683
|
-
const page = await walk(after);
|
|
2684
|
-
const text = {};
|
|
2685
|
-
for (const { path, content } of page.entries)
|
|
2686
|
-
if (content !== null)
|
|
2687
|
-
text[path] = content;
|
|
2688
|
-
const refused = await stepped(unreadableIn(text, Object.keys(text)));
|
|
2689
|
-
if (refused !== null)
|
|
2690
|
-
return refused;
|
|
2691
|
-
if (page.next === null)
|
|
2692
|
-
return null;
|
|
2693
|
-
after = page.next;
|
|
2694
|
-
}
|
|
2695
|
-
}
|
|
2696
|
-
async function validatedRoom(local, record, view, previous, overrides = null) {
|
|
2697
|
-
const composition = compositionOf(record);
|
|
2698
|
-
const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
|
|
2699
|
-
const lookup = overrides === null
|
|
2700
|
-
? lookupOf(local, record.key, view)
|
|
2701
|
-
: overriddenLookup(lookupOf(local, record.key, view), overrides);
|
|
2702
|
-
const texts = overrides === null
|
|
2703
|
-
? textsOf(local, record.key, view)
|
|
2704
|
-
: overriddenTexts(textsOf(local, record.key, view), overrides);
|
|
2705
|
-
const walk = overrides === null
|
|
2706
|
-
? walkOf(local, record.key, view)
|
|
2707
|
-
: overriddenWalk(walkOf(local, record.key, view), overrides);
|
|
2708
|
-
let changes = null;
|
|
2709
|
-
let reshaped = false;
|
|
2710
|
-
if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
|
|
2711
|
-
const [was, now] = await Promise.all([
|
|
2712
|
-
local.entries(record.key, 'confirmed', previous.changed),
|
|
2713
|
-
overrides === null
|
|
2714
|
-
? local.entries(record.key, view, previous.changed)
|
|
2715
|
-
: Promise.resolve([]),
|
|
2716
|
-
]);
|
|
2717
|
-
const before = new Map(was.map((entry) => [entry.path, entry]));
|
|
2718
|
-
const after = overrides === null
|
|
2719
|
-
? new Map(now.map((entry) => [entry.path, entry]))
|
|
2720
|
-
: overrides;
|
|
2721
|
-
changes = previous.changed.flatMap((path) => {
|
|
2722
|
-
const one = before.get(path) ?? null;
|
|
2723
|
-
const other = after.get(path) ?? null;
|
|
2724
|
-
reshaped ||= textDiffers(one, other);
|
|
2725
|
-
const held = textOf(one);
|
|
2726
|
-
const next = textOf(other);
|
|
2727
|
-
return held === next ? [] : [{ path, was: held, now: next }];
|
|
2728
|
-
});
|
|
2729
|
-
}
|
|
2730
|
-
const named = changes === null ? null : changes.map(({ path }) => path);
|
|
2731
|
-
const textChanged = changes === null || changes.length > 0 || reshaped;
|
|
2732
|
-
const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
|
|
2733
|
-
if (modeled && (textChanged || modelsChanged)) {
|
|
2734
|
-
const clean = previous !== null &&
|
|
2735
|
-
changes !== null &&
|
|
2736
|
-
(await changedCleanly(changes, compositionOf(previous.record), composition, {
|
|
2737
|
-
lookup,
|
|
2738
|
-
texts,
|
|
2739
|
-
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2740
|
-
}));
|
|
2741
|
-
if (clean)
|
|
2742
|
-
return null;
|
|
2743
|
-
const verdict = await validatedCollection(walk, lookup, composition);
|
|
2744
|
-
if (verdict.verdict === 'accept')
|
|
2745
|
-
return null;
|
|
2746
|
-
if (verdict.rule !== 'model-unavailable')
|
|
2747
|
-
return { kind: 'refused', refusal: verdict };
|
|
2748
|
-
return unreadableRoom(texts, walk, named);
|
|
2749
|
-
}
|
|
2750
|
-
if (textChanged)
|
|
2751
|
-
return unreadableRoom(texts, walk, named);
|
|
2752
|
-
return null;
|
|
2774
|
+
async function writtenLater(local, locator, key, paths, roomVersion) {
|
|
2775
|
+
if (paths.length === 0 || !older(locator, roomVersion))
|
|
2776
|
+
return new Set();
|
|
2777
|
+
const entries = await local.entries(key, 'confirmed', paths);
|
|
2778
|
+
return new Set(entries.flatMap(({ path, written }) => written !== null &&
|
|
2779
|
+
options.adapter.reached(locator, written, roomVersion) &&
|
|
2780
|
+
!options.adapter.reached(locator, roomVersion, written)
|
|
2781
|
+
? [path]
|
|
2782
|
+
: []));
|
|
2753
2783
|
}
|
|
2754
|
-
function refreshingOf(local, key, liveness) {
|
|
2784
|
+
function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
|
|
2785
|
+
const write = replaying ? finishingWrite : exclusive;
|
|
2755
2786
|
return {
|
|
2787
|
+
turn() {
|
|
2788
|
+
if (replaying)
|
|
2789
|
+
installing(locator);
|
|
2790
|
+
},
|
|
2756
2791
|
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2792
|
+
later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion),
|
|
2757
2793
|
async stage(page) {
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
const last =
|
|
2761
|
-
const
|
|
2794
|
+
for (let written = 0;;) {
|
|
2795
|
+
const end = batchEnd(page.entries, written);
|
|
2796
|
+
const last = end === page.entries.length;
|
|
2797
|
+
const entries = page.entries.slice(written, end);
|
|
2798
|
+
const staged = await write(async () => {
|
|
2762
2799
|
if (!liveness())
|
|
2763
2800
|
return false;
|
|
2764
|
-
await local.refresh(key, last
|
|
2801
|
+
await timed(stageBudget, () => local.refresh(key, composition, last
|
|
2765
2802
|
? { ...page, entries }
|
|
2766
|
-
: { entries, removals: [], folders: [], folderRemovals: [] });
|
|
2803
|
+
: { entries, removals: [], folders: [], folderRemovals: [] }));
|
|
2767
2804
|
return true;
|
|
2768
2805
|
});
|
|
2769
2806
|
if (!staged)
|
|
2770
2807
|
return false;
|
|
2808
|
+
if (last)
|
|
2809
|
+
return true;
|
|
2810
|
+
written = end;
|
|
2771
2811
|
}
|
|
2772
|
-
return true;
|
|
2773
2812
|
},
|
|
2774
2813
|
};
|
|
2775
2814
|
}
|
|
2776
|
-
function
|
|
2777
|
-
|
|
2815
|
+
function batchEnd(entries, from) {
|
|
2816
|
+
let end = from;
|
|
2778
2817
|
let spent = 0;
|
|
2779
|
-
|
|
2780
|
-
const size = (
|
|
2781
|
-
if (spent + size >
|
|
2782
|
-
|
|
2783
|
-
spent = 0;
|
|
2784
|
-
}
|
|
2785
|
-
batches.at(-1).push(entry);
|
|
2818
|
+
while (end < entries.length) {
|
|
2819
|
+
const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
|
|
2820
|
+
if (spent + size > stageBudget.bytes && end > from)
|
|
2821
|
+
break;
|
|
2786
2822
|
spent += size;
|
|
2823
|
+
end += 1;
|
|
2824
|
+
}
|
|
2825
|
+
return end;
|
|
2826
|
+
}
|
|
2827
|
+
async function timed(budget, work) {
|
|
2828
|
+
const started = performance.now();
|
|
2829
|
+
const done = await work();
|
|
2830
|
+
const spent = performance.now() - started;
|
|
2831
|
+
if (spent < budget.aim / 2)
|
|
2832
|
+
budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
|
|
2833
|
+
else if (spent > budget.aim * 2) {
|
|
2834
|
+
budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
|
|
2835
|
+
}
|
|
2836
|
+
return done;
|
|
2837
|
+
}
|
|
2838
|
+
function installWrite(work) {
|
|
2839
|
+
return installTurn(installTurns.starting, work);
|
|
2840
|
+
}
|
|
2841
|
+
function finishingWrite(work) {
|
|
2842
|
+
return installTurn(installTurns.finishing, work);
|
|
2843
|
+
}
|
|
2844
|
+
function installTurn(queue, work) {
|
|
2845
|
+
return new Promise((resolve, reject) => {
|
|
2846
|
+
queue.push(() => void exclusive(work)
|
|
2847
|
+
.then(resolve, reject)
|
|
2848
|
+
.finally(() => {
|
|
2849
|
+
installTurns.busy = false;
|
|
2850
|
+
nextInstallTurn();
|
|
2851
|
+
}));
|
|
2852
|
+
nextInstallTurn();
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
function nextInstallTurn() {
|
|
2856
|
+
if (installTurns.busy || installTurns.ahead > 0)
|
|
2857
|
+
return;
|
|
2858
|
+
const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
|
|
2859
|
+
if (turn === undefined)
|
|
2860
|
+
return;
|
|
2861
|
+
installTurns.busy = true;
|
|
2862
|
+
turn();
|
|
2863
|
+
}
|
|
2864
|
+
async function ahead(work) {
|
|
2865
|
+
let taken = false;
|
|
2866
|
+
const hold = {
|
|
2867
|
+
take() {
|
|
2868
|
+
if (taken)
|
|
2869
|
+
return;
|
|
2870
|
+
taken = true;
|
|
2871
|
+
installTurns.ahead += 1;
|
|
2872
|
+
},
|
|
2873
|
+
release() {
|
|
2874
|
+
if (!taken)
|
|
2875
|
+
return;
|
|
2876
|
+
taken = false;
|
|
2877
|
+
installTurns.ahead -= 1;
|
|
2878
|
+
resumeInstall();
|
|
2879
|
+
},
|
|
2880
|
+
};
|
|
2881
|
+
hold.take();
|
|
2882
|
+
try {
|
|
2883
|
+
return await work(hold);
|
|
2884
|
+
}
|
|
2885
|
+
finally {
|
|
2886
|
+
hold.release();
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
function resumeInstall() {
|
|
2890
|
+
if (installTurns.ahead > 0)
|
|
2891
|
+
return;
|
|
2892
|
+
void yielded().then(nextInstallTurn);
|
|
2893
|
+
}
|
|
2894
|
+
async function heldStream() {
|
|
2895
|
+
while (streamsFree === 0)
|
|
2896
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
2897
|
+
streamsFree -= 1;
|
|
2898
|
+
return once(() => {
|
|
2899
|
+
streamsFree += 1;
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2902
|
+
async function unqueued() {
|
|
2903
|
+
while (queuedText >= QUEUED_TEXT) {
|
|
2904
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
2787
2905
|
}
|
|
2788
|
-
return batches;
|
|
2789
2906
|
}
|
|
2790
|
-
|
|
2907
|
+
function once(release) {
|
|
2908
|
+
let released = false;
|
|
2909
|
+
return () => {
|
|
2910
|
+
if (released)
|
|
2911
|
+
return;
|
|
2912
|
+
released = true;
|
|
2913
|
+
release();
|
|
2914
|
+
for (const admit of installWaiters.splice(0))
|
|
2915
|
+
admit();
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
function installing(locator) {
|
|
2919
|
+
installs.add(locator);
|
|
2920
|
+
queueReadiness();
|
|
2921
|
+
}
|
|
2922
|
+
function untransferred(locator) {
|
|
2923
|
+
transfers.delete(locator);
|
|
2924
|
+
arrivals.delete(locator);
|
|
2925
|
+
streaming.delete(locator);
|
|
2926
|
+
underived.delete(locator);
|
|
2927
|
+
unindexed.delete(locator);
|
|
2928
|
+
}
|
|
2929
|
+
function setBack(locator) {
|
|
2930
|
+
const now = bytesOf(locator, true);
|
|
2931
|
+
const earlier = startedOver.get(locator);
|
|
2932
|
+
startedOver.set(locator, {
|
|
2933
|
+
done: Math.max(now.done, earlier?.done ?? 0),
|
|
2934
|
+
total: Math.max(now.total, earlier?.total ?? 0),
|
|
2935
|
+
});
|
|
2936
|
+
}
|
|
2937
|
+
function owedIndexes(locator, bytes, text) {
|
|
2938
|
+
if (bytes <= 0 || text <= 0)
|
|
2939
|
+
return;
|
|
2940
|
+
unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
|
|
2941
|
+
}
|
|
2942
|
+
function built(locator, { done, text }) {
|
|
2943
|
+
const owed = unindexed.get(locator) ?? [];
|
|
2944
|
+
let left = text;
|
|
2945
|
+
while (!done && left > 0 && owed.length > 0) {
|
|
2946
|
+
const [first] = owed;
|
|
2947
|
+
if (left >= first.text) {
|
|
2948
|
+
left -= first.text;
|
|
2949
|
+
owed.shift();
|
|
2950
|
+
continue;
|
|
2951
|
+
}
|
|
2952
|
+
owed[0] = {
|
|
2953
|
+
bytes: (first.bytes * (first.text - left)) / first.text,
|
|
2954
|
+
text: first.text - left,
|
|
2955
|
+
};
|
|
2956
|
+
left = 0;
|
|
2957
|
+
}
|
|
2958
|
+
if (done || owed.length === 0)
|
|
2959
|
+
unindexed.delete(locator);
|
|
2960
|
+
queueReadiness();
|
|
2961
|
+
}
|
|
2962
|
+
function installed(locator) {
|
|
2963
|
+
if (installs.delete(locator))
|
|
2964
|
+
queueReadiness();
|
|
2965
|
+
}
|
|
2966
|
+
async function pullRefresh(authorized, held, liveness, hold, refresh) {
|
|
2791
2967
|
if (!liveness())
|
|
2792
2968
|
return unconfirmed;
|
|
2793
|
-
const turn = generation;
|
|
2794
2969
|
const { record } = held;
|
|
2795
|
-
const
|
|
2970
|
+
const from = standingOf(record.locator);
|
|
2796
2971
|
const local = await openLocal();
|
|
2797
2972
|
try {
|
|
2798
2973
|
await exclusive(() => local.unrefresh(record.key));
|
|
2799
|
-
const result = await advancement(
|
|
2974
|
+
const result = await advancement(async (authorization, request) => {
|
|
2975
|
+
hold.release();
|
|
2976
|
+
try {
|
|
2977
|
+
return await refresh(authorization, request);
|
|
2978
|
+
}
|
|
2979
|
+
finally {
|
|
2980
|
+
hold.take();
|
|
2981
|
+
}
|
|
2982
|
+
}, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness));
|
|
2800
2983
|
if (result.kind !== 'advanced') {
|
|
2801
2984
|
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2802
2985
|
return result;
|
|
2803
2986
|
}
|
|
2804
|
-
const refused = await validatedRoom(local, result.record, 'refreshed', {
|
|
2805
|
-
record,
|
|
2806
|
-
changed: [...result.changed],
|
|
2807
|
-
});
|
|
2808
|
-
if (refused !== null) {
|
|
2809
|
-
journal.record(`transfer refresh ${record.locator} refused`);
|
|
2810
|
-
return refused;
|
|
2811
|
-
}
|
|
2812
2987
|
journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
|
|
2813
|
-
return {
|
|
2988
|
+
return {
|
|
2989
|
+
kind: 'advanced',
|
|
2990
|
+
record: result.record,
|
|
2991
|
+
from,
|
|
2992
|
+
remodelled: result.record.model !== record.model,
|
|
2993
|
+
};
|
|
2814
2994
|
}
|
|
2815
2995
|
catch (error) {
|
|
2996
|
+
if (refusedForSpace(error))
|
|
2997
|
+
return unconfirmed;
|
|
2816
2998
|
journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
|
|
2817
2999
|
throw error;
|
|
2818
3000
|
}
|
|
2819
|
-
finally {
|
|
2820
|
-
reads.finished(record.locator, turn);
|
|
2821
|
-
}
|
|
2822
3001
|
}
|
|
2823
3002
|
const heldContents = new Map();
|
|
2824
3003
|
function heldContentOf(local, key) {
|
|
@@ -2836,63 +3015,115 @@ export function composeRuntime(options) {
|
|
|
2836
3015
|
heldContents.set(key, made);
|
|
2837
3016
|
return made;
|
|
2838
3017
|
}
|
|
3018
|
+
async function abandoned(local, locator) {
|
|
3019
|
+
await local.abandon(locator);
|
|
3020
|
+
setBack(locator);
|
|
3021
|
+
untransferred(locator);
|
|
3022
|
+
liveQueries.recover();
|
|
3023
|
+
sweep();
|
|
3024
|
+
}
|
|
2839
3025
|
function staging(local, liveness) {
|
|
3026
|
+
const derived = async (locator, key, composition) => {
|
|
3027
|
+
underived.delete(locator);
|
|
3028
|
+
for (;;) {
|
|
3029
|
+
if (!liveness())
|
|
3030
|
+
return false;
|
|
3031
|
+
const done = await finishingWrite(async () => liveness()
|
|
3032
|
+
? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
|
|
3033
|
+
: null);
|
|
3034
|
+
if (done === null)
|
|
3035
|
+
return false;
|
|
3036
|
+
built(locator, done);
|
|
3037
|
+
if (done.done)
|
|
3038
|
+
return true;
|
|
3039
|
+
}
|
|
3040
|
+
};
|
|
2840
3041
|
return {
|
|
2841
3042
|
held: (locator) => transfers.get(locator) ?? null,
|
|
3043
|
+
async turn(locator, step) {
|
|
3044
|
+
installing(locator);
|
|
3045
|
+
return step === 'downloading' ? heldStream() : () => { };
|
|
3046
|
+
},
|
|
3047
|
+
unqueued,
|
|
3048
|
+
queued(text) {
|
|
3049
|
+
queuedText += text;
|
|
3050
|
+
return once(() => {
|
|
3051
|
+
queuedText -= text;
|
|
3052
|
+
});
|
|
3053
|
+
},
|
|
3054
|
+
arriving(locator) {
|
|
3055
|
+
let ended = false;
|
|
3056
|
+
return {
|
|
3057
|
+
at(bytes) {
|
|
3058
|
+
if (ended)
|
|
3059
|
+
return;
|
|
3060
|
+
arrivals.set(locator, bytes);
|
|
3061
|
+
streaming.add(locator);
|
|
3062
|
+
const now = performance.now();
|
|
3063
|
+
if (now - arrivalReported < ARRIVAL_REPORT_MS)
|
|
3064
|
+
return;
|
|
3065
|
+
arrivalReported = now;
|
|
3066
|
+
queueReadiness();
|
|
3067
|
+
},
|
|
3068
|
+
end() {
|
|
3069
|
+
if (ended)
|
|
3070
|
+
return;
|
|
3071
|
+
ended = true;
|
|
3072
|
+
if (streaming.delete(locator))
|
|
3073
|
+
queueReadiness();
|
|
3074
|
+
},
|
|
3075
|
+
};
|
|
3076
|
+
},
|
|
3077
|
+
streamed: options.adapter.roomStream !== undefined,
|
|
2842
3078
|
async stage(record, page, composition) {
|
|
2843
|
-
const
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
if (!liveness())
|
|
2850
|
-
return null;
|
|
2851
|
-
if (index === 0 && !(await admitted(local, record.locator, bytes)))
|
|
2852
|
-
return null;
|
|
2853
|
-
try {
|
|
2854
|
-
return await local.stage(record, composition, { entries, folders: last ? page.folders : [] }, key, last);
|
|
2855
|
-
}
|
|
2856
|
-
catch (error) {
|
|
2857
|
-
if (!exceeded(error))
|
|
2858
|
-
throw error;
|
|
2859
|
-
await exceededOn(record.locator, bytes);
|
|
2860
|
-
return null;
|
|
2861
|
-
}
|
|
2862
|
-
});
|
|
2863
|
-
if (staged === null)
|
|
3079
|
+
const { locator } = record;
|
|
3080
|
+
installing(locator);
|
|
3081
|
+
if (!liveness())
|
|
3082
|
+
return null;
|
|
3083
|
+
const key = await installWrite(async () => {
|
|
3084
|
+
if (!liveness())
|
|
2864
3085
|
return null;
|
|
2865
|
-
|
|
2866
|
-
}
|
|
2867
|
-
|
|
3086
|
+
return local.stage(record, composition, page);
|
|
3087
|
+
});
|
|
3088
|
+
if (key === null || !liveness())
|
|
3089
|
+
return null;
|
|
3090
|
+
const arrived = !transfers.has(locator);
|
|
3091
|
+
const prior = transfers.get(locator)?.done.bytes ?? 0;
|
|
3092
|
+
const held = { ...record, key };
|
|
3093
|
+
transfers.set(locator, held);
|
|
3094
|
+
if (arrived)
|
|
3095
|
+
liveQueries.recover();
|
|
3096
|
+
installing(locator);
|
|
2868
3097
|
liveQueries.recover();
|
|
2869
|
-
|
|
3098
|
+
const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
|
|
3099
|
+
owedIndexes(locator, record.done.bytes - prior, text);
|
|
3100
|
+
const owed = text + (underived.get(locator) ?? 0);
|
|
3101
|
+
underived.set(locator, owed);
|
|
3102
|
+
if (owed < deriveBudget.bytes)
|
|
3103
|
+
return held;
|
|
3104
|
+
return (await derived(locator, key, composition)) ? held : null;
|
|
2870
3105
|
},
|
|
2871
|
-
|
|
3106
|
+
derive: derived,
|
|
3107
|
+
async refreshing(locator, key, composition) {
|
|
2872
3108
|
await exclusive(() => local.unrefresh(key));
|
|
2873
|
-
return refreshingOf(local, key, liveness);
|
|
3109
|
+
return refreshingOf(local, locator, key, composition, liveness, true);
|
|
2874
3110
|
},
|
|
2875
3111
|
abandon: (locator) => exclusive(async () => {
|
|
2876
|
-
if (
|
|
2877
|
-
|
|
2878
|
-
await local.abandon(locator);
|
|
2879
|
-
transfers.delete(locator);
|
|
2880
|
-
liveQueries.recover();
|
|
2881
|
-
sweep();
|
|
3112
|
+
if (liveness())
|
|
3113
|
+
await abandoned(local, locator);
|
|
2882
3114
|
}),
|
|
2883
|
-
setAside:
|
|
3115
|
+
setAside: async (locator) => {
|
|
2884
3116
|
if (!liveness())
|
|
2885
3117
|
return;
|
|
2886
|
-
|
|
2887
|
-
|
|
3118
|
+
setBack(locator);
|
|
3119
|
+
untransferred(locator);
|
|
2888
3120
|
liveQueries.recover();
|
|
2889
|
-
}
|
|
3121
|
+
},
|
|
2890
3122
|
release: (key) => exclusive(async () => {
|
|
2891
3123
|
await local.release(key);
|
|
2892
3124
|
sweep();
|
|
2893
3125
|
}),
|
|
2894
3126
|
content: (key) => heldContentOf(local, key),
|
|
2895
|
-
validate: (record, view) => validatedRoom(local, record, view, null),
|
|
2896
3127
|
};
|
|
2897
3128
|
}
|
|
2898
3129
|
function reusable(room, local) {
|
|
@@ -2904,30 +3135,35 @@ export function composeRuntime(options) {
|
|
|
2904
3135
|
async function pullBootstrap(authorized, room, local, liveness) {
|
|
2905
3136
|
if (!liveness())
|
|
2906
3137
|
return unconfirmed;
|
|
2907
|
-
const turn = generation;
|
|
2908
3138
|
if (replicas.has(room.locator)) {
|
|
2909
3139
|
catching.add(room.locator);
|
|
2910
3140
|
liveQueries.recover();
|
|
2911
3141
|
}
|
|
2912
3142
|
try {
|
|
2913
|
-
const result = await bootstrapRoom(reads, authorized, room, {
|
|
2914
|
-
collection: modelOf(room.locator),
|
|
2915
|
-
selection: modelOf(room.locator, 'typed-selection'),
|
|
2916
|
-
}, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
|
|
3143
|
+
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));
|
|
2917
3144
|
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
2918
3145
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
2919
|
-
if (result.kind
|
|
3146
|
+
if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
|
|
3147
|
+
caughtUpTo(room.locator);
|
|
3148
|
+
installed(room.locator);
|
|
3149
|
+
await abandoned(local, room.locator);
|
|
3150
|
+
journal.record(`transfer bootstrap ${room.locator} older than shown`);
|
|
3151
|
+
return { kind: 'transport-uncertainty' };
|
|
3152
|
+
}
|
|
3153
|
+
if (result.kind !== 'staged') {
|
|
2920
3154
|
caughtUpTo(room.locator);
|
|
3155
|
+
installed(room.locator);
|
|
3156
|
+
}
|
|
2921
3157
|
return result;
|
|
2922
3158
|
}
|
|
2923
3159
|
catch (error) {
|
|
2924
|
-
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
2925
3160
|
caughtUpTo(room.locator);
|
|
3161
|
+
installed(room.locator);
|
|
3162
|
+
if (refusedForSpace(error))
|
|
3163
|
+
return unconfirmed;
|
|
3164
|
+
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
2926
3165
|
throw error;
|
|
2927
3166
|
}
|
|
2928
|
-
finally {
|
|
2929
|
-
reads.finished(room.locator, turn);
|
|
2930
|
-
}
|
|
2931
3167
|
}
|
|
2932
3168
|
function caughtUpTo(locator) {
|
|
2933
3169
|
if (catching.delete(locator))
|
|
@@ -2935,72 +3171,83 @@ export function composeRuntime(options) {
|
|
|
2935
3171
|
}
|
|
2936
3172
|
function behind(room) {
|
|
2937
3173
|
const held = replicas.get(room.locator)?.record.roomVersion;
|
|
2938
|
-
return
|
|
2939
|
-
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
3174
|
+
return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
|
|
2940
3175
|
}
|
|
2941
|
-
|
|
2942
|
-
const
|
|
2943
|
-
|
|
2944
|
-
if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
|
|
2945
|
-
return withheld;
|
|
2946
|
-
}
|
|
2947
|
-
return advanceOnce(room, local, authorized, live);
|
|
3176
|
+
function standingOf(locator) {
|
|
3177
|
+
const held = replicas.get(locator);
|
|
3178
|
+
return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
|
|
2948
3179
|
}
|
|
2949
|
-
|
|
2950
|
-
|
|
3180
|
+
function stands(locator, from) {
|
|
3181
|
+
const now = standingOf(locator);
|
|
3182
|
+
return now.key === from.key && now.confirmed === from.confirmed;
|
|
3183
|
+
}
|
|
3184
|
+
async function advance(room, local, authorized, running) {
|
|
3185
|
+
const live = () => running() && !full;
|
|
3186
|
+
if (!live())
|
|
2951
3187
|
return unconfirmed;
|
|
2952
3188
|
const held = replicas.get(room.locator);
|
|
2953
|
-
if (held !== undefined) {
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
emitRooms();
|
|
2965
|
-
}
|
|
2966
|
-
return null;
|
|
2967
|
-
}
|
|
2968
|
-
if (refreshed.kind === 'advanced') {
|
|
2969
|
-
const landing = { kind: 'refreshed', base: refreshed.base };
|
|
2970
|
-
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
2971
|
-
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
2972
|
-
const reached = now !== undefined &&
|
|
2973
|
-
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
2974
|
-
return reached ? null : advance(room, local, authorized, live);
|
|
2975
|
-
}
|
|
2976
|
-
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
2977
|
-
return null;
|
|
2978
|
-
}
|
|
2979
|
-
if (refreshed.kind !== 'beyond-horizon')
|
|
3189
|
+
if (held !== undefined && options.adapter.roomStream !== undefined) {
|
|
3190
|
+
stale.delete(room.locator);
|
|
3191
|
+
name(room.locator);
|
|
3192
|
+
return null;
|
|
3193
|
+
}
|
|
3194
|
+
const refresh = reads.refresh;
|
|
3195
|
+
if (held !== undefined && refresh !== undefined) {
|
|
3196
|
+
const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
|
|
3197
|
+
if (refreshed === 'behind')
|
|
3198
|
+
return advance(room, local, authorized, live);
|
|
3199
|
+
if (refreshed !== 'beyond-horizon')
|
|
2980
3200
|
return refreshed;
|
|
2981
|
-
if (
|
|
2982
|
-
owed.add(room.locator);
|
|
2983
|
-
liveQueries.recover();
|
|
2984
|
-
return unconfirmed;
|
|
2985
|
-
}
|
|
2986
|
-
if (reloadsDue.get(room.locator) === generation)
|
|
3201
|
+
if (reloadsDue.has(room.locator))
|
|
2987
3202
|
return unconfirmed;
|
|
3203
|
+
replaced([room.locator]);
|
|
2988
3204
|
}
|
|
3205
|
+
const from = standingOf(room.locator);
|
|
2989
3206
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
3207
|
+
if (bootstrapped.kind !== 'staged')
|
|
3208
|
+
return live() ? bootstrapped : unconfirmed;
|
|
3209
|
+
const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
|
|
3210
|
+
if (await publish(local, room, bootstrapped.record, landing, live)) {
|
|
3211
|
+
return live() ? null : unconfirmed;
|
|
3212
|
+
}
|
|
3213
|
+
return reachedBy(room.locator, bootstrapped.record.roomVersion)
|
|
3214
|
+
? null
|
|
3215
|
+
: advance(room, local, authorized, live);
|
|
3216
|
+
}
|
|
3217
|
+
async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
|
|
3218
|
+
const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
|
|
2990
3219
|
if (!live())
|
|
2991
3220
|
return unconfirmed;
|
|
2992
|
-
if (
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
3221
|
+
if (refreshed.kind === 'still-current') {
|
|
3222
|
+
stale.delete(room.locator);
|
|
3223
|
+
if (unreadable.delete(room.locator)) {
|
|
3224
|
+
scopedRefusals.delete(room.locator);
|
|
3225
|
+
list(room, true);
|
|
3226
|
+
emitRooms();
|
|
3227
|
+
}
|
|
3228
|
+
return null;
|
|
3229
|
+
}
|
|
3230
|
+
if (refreshed.kind === 'advanced') {
|
|
3231
|
+
const landing = {
|
|
3232
|
+
kind: 'refreshed',
|
|
3233
|
+
from: refreshed.from,
|
|
3234
|
+
remodelled: refreshed.remodelled,
|
|
3235
|
+
};
|
|
3236
|
+
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3237
|
+
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3238
|
+
const reached = now !== undefined &&
|
|
3239
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3240
|
+
return reached ? null : 'behind';
|
|
3241
|
+
}
|
|
3242
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3243
|
+
return null;
|
|
3244
|
+
}
|
|
3245
|
+
return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
|
|
2996
3246
|
}
|
|
2997
3247
|
async function withhold(withheld, room, local, progress) {
|
|
2998
|
-
if (withheld.kind === 'unconfirmed')
|
|
3248
|
+
if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
|
|
2999
3249
|
return;
|
|
3000
|
-
|
|
3001
|
-
stale.delete(room.locator);
|
|
3002
|
-
else
|
|
3003
|
-
stale.add(room.locator);
|
|
3250
|
+
stale.add(room.locator);
|
|
3004
3251
|
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
3005
3252
|
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
3006
3253
|
repairable.set(room.locator, reset);
|
|
@@ -3022,30 +3269,10 @@ export function composeRuntime(options) {
|
|
|
3022
3269
|
list(room, false);
|
|
3023
3270
|
emitRooms();
|
|
3024
3271
|
}
|
|
3025
|
-
else if (withheld.kind === 'refused') {
|
|
3026
|
-
refusedSource(room, withheld);
|
|
3027
|
-
}
|
|
3028
3272
|
else if (withheld.kind === 'revoked-authorization') {
|
|
3029
3273
|
await refuse('revoked-authorization');
|
|
3030
3274
|
}
|
|
3031
3275
|
}
|
|
3032
|
-
function refusedSource(room, { refusal }) {
|
|
3033
|
-
sourceRefusals.set(room.locator, {
|
|
3034
|
-
code: refusal.rule,
|
|
3035
|
-
retryable: false,
|
|
3036
|
-
fields: {
|
|
3037
|
-
file: refusal.file,
|
|
3038
|
-
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
3039
|
-
...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
|
|
3040
|
-
},
|
|
3041
|
-
});
|
|
3042
|
-
declarationRefusals.delete(room.locator);
|
|
3043
|
-
unreadable.add(room.locator);
|
|
3044
|
-
list(room, false);
|
|
3045
|
-
emitRooms();
|
|
3046
|
-
moveTo({ ...status, ...attention() });
|
|
3047
|
-
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
3048
|
-
}
|
|
3049
3276
|
function leaving(batch, result) {
|
|
3050
3277
|
if (result.kind === 'expired')
|
|
3051
3278
|
return { batch, returned: returnedBy('expired') };
|
|
@@ -3067,8 +3294,8 @@ export function composeRuntime(options) {
|
|
|
3067
3294
|
function settling(acceptance) {
|
|
3068
3295
|
return acceptance !== null && 'apply' in acceptance;
|
|
3069
3296
|
}
|
|
3070
|
-
function
|
|
3071
|
-
return settling(acceptance) ?
|
|
3297
|
+
function failureOf(acceptance) {
|
|
3298
|
+
return settling(acceptance) ? null : acceptance;
|
|
3072
3299
|
}
|
|
3073
3300
|
async function acceptance(room, local, submitted, accepted) {
|
|
3074
3301
|
if (!continuing())
|
|
@@ -3081,7 +3308,8 @@ export function composeRuntime(options) {
|
|
|
3081
3308
|
logAccepted(room.locator, submitted, accepted);
|
|
3082
3309
|
let record = current.record;
|
|
3083
3310
|
let changes;
|
|
3084
|
-
if (!options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
3311
|
+
if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
|
|
3312
|
+
!older(room.locator, accepted.roomVersion)) {
|
|
3085
3313
|
const paths = [...returnedPaths(submitted, accepted)];
|
|
3086
3314
|
const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
|
|
3087
3315
|
const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
|
|
@@ -3091,34 +3319,6 @@ export function composeRuntime(options) {
|
|
|
3091
3319
|
? accepted.roomVersion
|
|
3092
3320
|
: held,
|
|
3093
3321
|
};
|
|
3094
|
-
const overrides = new Map([
|
|
3095
|
-
...back.set.map((entry) => [entry.path, entry]),
|
|
3096
|
-
...back.removed.map((path) => [path, null]),
|
|
3097
|
-
]);
|
|
3098
|
-
const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
|
|
3099
|
-
if (refused !== null) {
|
|
3100
|
-
const key = pendingKey(room.locator, submitted.id);
|
|
3101
|
-
return {
|
|
3102
|
-
locator: room.locator,
|
|
3103
|
-
left: [],
|
|
3104
|
-
commit: {
|
|
3105
|
-
rooms: [],
|
|
3106
|
-
writes: [
|
|
3107
|
-
{
|
|
3108
|
-
locator: room.locator,
|
|
3109
|
-
accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
|
|
3110
|
-
},
|
|
3111
|
-
],
|
|
3112
|
-
},
|
|
3113
|
-
refused,
|
|
3114
|
-
apply() {
|
|
3115
|
-
const state = pendings.get(key);
|
|
3116
|
-
if (state !== undefined && !fences.has(room.locator))
|
|
3117
|
-
pendings.set(key, { ...state, accepted: accepted.roomVersion });
|
|
3118
|
-
return changeSet(room.locator, { queue: true });
|
|
3119
|
-
},
|
|
3120
|
-
};
|
|
3121
|
-
}
|
|
3122
3322
|
changes = back;
|
|
3123
3323
|
}
|
|
3124
3324
|
const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
@@ -3126,10 +3326,9 @@ export function composeRuntime(options) {
|
|
|
3126
3326
|
...settled,
|
|
3127
3327
|
apply(answer) {
|
|
3128
3328
|
const set = settled.apply(answer);
|
|
3329
|
+
showing(room.locator, accepted.roomVersion);
|
|
3129
3330
|
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3130
3331
|
unreadable.delete(room.locator);
|
|
3131
|
-
sourceRefusals.delete(room.locator);
|
|
3132
|
-
declarationRefusals.delete(room.locator);
|
|
3133
3332
|
list(room, true);
|
|
3134
3333
|
return set;
|
|
3135
3334
|
},
|
|
@@ -3147,13 +3346,9 @@ export function composeRuntime(options) {
|
|
|
3147
3346
|
const failed = await exclusive(async () => {
|
|
3148
3347
|
const adopting = await acceptance(room, local, submitted, accepted);
|
|
3149
3348
|
await adoptedTogether(local, [adopting]);
|
|
3150
|
-
return
|
|
3349
|
+
return failureOf(adopting);
|
|
3151
3350
|
});
|
|
3152
|
-
|
|
3153
|
-
return true;
|
|
3154
|
-
if (failed.kind === 'refused' && continuing())
|
|
3155
|
-
refusedSource(room, failed);
|
|
3156
|
-
return false;
|
|
3351
|
+
return failed === null;
|
|
3157
3352
|
}
|
|
3158
3353
|
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
3159
3354
|
const other = discovered.get(partner);
|
|
@@ -3167,18 +3362,10 @@ export function composeRuntime(options) {
|
|
|
3167
3362
|
...accepted.destination,
|
|
3168
3363
|
});
|
|
3169
3364
|
await adoptedTogether(local, [left, arrived]);
|
|
3170
|
-
|
|
3171
|
-
if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
|
|
3172
|
-
refusedSource(other, refusedArrival);
|
|
3173
|
-
}
|
|
3174
|
-
return refusalOf(left);
|
|
3365
|
+
return failureOf(left);
|
|
3175
3366
|
});
|
|
3176
3367
|
submitLater(partner);
|
|
3177
|
-
|
|
3178
|
-
return true;
|
|
3179
|
-
if (failed.kind === 'refused' && continuing())
|
|
3180
|
-
refusedSource(room, failed);
|
|
3181
|
-
return false;
|
|
3368
|
+
return failed === null;
|
|
3182
3369
|
}
|
|
3183
3370
|
function queueIn(locator) {
|
|
3184
3371
|
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
@@ -3301,8 +3488,6 @@ export function composeRuntime(options) {
|
|
|
3301
3488
|
if (replica === undefined || head === undefined)
|
|
3302
3489
|
return 'done';
|
|
3303
3490
|
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
3304
|
-
if (state?.accepted !== undefined)
|
|
3305
|
-
return 'done';
|
|
3306
3491
|
const partner = partnerOf(head, room.locator);
|
|
3307
3492
|
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3308
3493
|
submitLater(partner);
|
|
@@ -3416,12 +3601,12 @@ export function composeRuntime(options) {
|
|
|
3416
3601
|
}
|
|
3417
3602
|
if (result.kind === 'expired') {
|
|
3418
3603
|
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
3419
|
-
const there = other === undefined ? null : await
|
|
3604
|
+
const there = other === undefined ? null : await advanced(other, local, authorized);
|
|
3420
3605
|
if (other !== undefined && there !== null) {
|
|
3421
3606
|
await withhold(there, other, local, progress);
|
|
3422
3607
|
return 'done';
|
|
3423
3608
|
}
|
|
3424
|
-
const withheld = await
|
|
3609
|
+
const withheld = await advanced(room, local, authorized);
|
|
3425
3610
|
if (withheld !== null) {
|
|
3426
3611
|
await withhold(withheld, room, local, progress);
|
|
3427
3612
|
return 'done';
|
|
@@ -3443,8 +3628,8 @@ export function composeRuntime(options) {
|
|
|
3443
3628
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
3444
3629
|
const left = leaving(head, result);
|
|
3445
3630
|
const refreshed = async () => ({
|
|
3446
|
-
there: other === undefined ? null : await
|
|
3447
|
-
here: await
|
|
3631
|
+
there: other === undefined ? null : await advanced(other, local, authorized),
|
|
3632
|
+
here: await advanced(room, local, authorized),
|
|
3448
3633
|
});
|
|
3449
3634
|
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
3450
3635
|
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
@@ -3515,6 +3700,7 @@ export function composeRuntime(options) {
|
|
|
3515
3700
|
locator: room.locator,
|
|
3516
3701
|
key: before.record.key,
|
|
3517
3702
|
roots: before.record.typedSelection?.roots ?? [],
|
|
3703
|
+
composed: composedOf(composed(before.record)),
|
|
3518
3704
|
overlay: { set: [], cleared: [...before.overlaid] },
|
|
3519
3705
|
},
|
|
3520
3706
|
]),
|
|
@@ -3547,39 +3733,37 @@ export function composeRuntime(options) {
|
|
|
3547
3733
|
if (signal.aborted)
|
|
3548
3734
|
return;
|
|
3549
3735
|
const progress = { complete: true, reachable: true };
|
|
3550
|
-
await roomTask(room.locator,
|
|
3551
|
-
await Promise.race([
|
|
3552
|
-
reload(room, local, progress, () => live() && !signal.aborted),
|
|
3553
|
-
abandonedOn(signal),
|
|
3554
|
-
]);
|
|
3555
|
-
});
|
|
3736
|
+
await roomTask(room.locator, (live) => reload(room, local, progress, live));
|
|
3556
3737
|
if (signal.aborted)
|
|
3557
3738
|
return;
|
|
3558
3739
|
await rest(progress);
|
|
3559
3740
|
}
|
|
3560
3741
|
}
|
|
3561
3742
|
function awaitingAnswer(locator) {
|
|
3743
|
+
answering.get(locator)?.release();
|
|
3562
3744
|
let release = () => { };
|
|
3563
|
-
const
|
|
3745
|
+
const answered = new Promise((resolve) => {
|
|
3564
3746
|
release = resolve;
|
|
3565
3747
|
});
|
|
3566
|
-
|
|
3748
|
+
const awaited = { answered, release };
|
|
3749
|
+
answering.set(locator, awaited);
|
|
3567
3750
|
return () => {
|
|
3568
|
-
if (answering.get(locator)
|
|
3751
|
+
if (answering.get(locator) === awaited)
|
|
3569
3752
|
answering.delete(locator);
|
|
3570
3753
|
release();
|
|
3571
3754
|
};
|
|
3572
3755
|
}
|
|
3573
|
-
|
|
3574
|
-
const
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3756
|
+
function declaredAsHeld(room) {
|
|
3757
|
+
const held = replicas.get(room.locator)?.record;
|
|
3758
|
+
return (held === undefined ||
|
|
3759
|
+
(JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
|
|
3760
|
+
JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
|
|
3761
|
+
JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
|
|
3762
|
+
}
|
|
3763
|
+
async function retainModel(local, room, next) {
|
|
3764
|
+
const key = room.locator;
|
|
3765
|
+
const previous = modelOf(room.locator);
|
|
3766
|
+
if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
|
|
3583
3767
|
return;
|
|
3584
3768
|
if (next.status === 'unavailable') {
|
|
3585
3769
|
invalidModels.add(key);
|
|
@@ -3591,26 +3775,13 @@ export function composeRuntime(options) {
|
|
|
3591
3775
|
const held = replicas.get(room.locator);
|
|
3592
3776
|
let changed = null;
|
|
3593
3777
|
if (held !== undefined) {
|
|
3594
|
-
const
|
|
3778
|
+
const record = {
|
|
3595
3779
|
...held.record,
|
|
3596
3780
|
operations: room.operations,
|
|
3597
3781
|
replica: room.replica,
|
|
3598
3782
|
typedSelection: room.typedSelection ?? null,
|
|
3599
|
-
|
|
3783
|
+
model: next,
|
|
3600
3784
|
};
|
|
3601
|
-
let read = 'staged';
|
|
3602
|
-
if (!sameMembership(declared, held.record)) {
|
|
3603
|
-
const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
|
|
3604
|
-
read =
|
|
3605
|
-
verdict.verdict !== 'refuse'
|
|
3606
|
-
? 'staged'
|
|
3607
|
-
: verdict.rule === 'model-unavailable'
|
|
3608
|
-
? 'deferred'
|
|
3609
|
-
: { kind: 'refused', refusal: verdict };
|
|
3610
|
-
}
|
|
3611
|
-
const record = read === 'staged'
|
|
3612
|
-
? declared
|
|
3613
|
-
: { ...declared, typedSelection: held.record.typedSelection };
|
|
3614
3785
|
const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
|
|
3615
3786
|
const replayedLayer = await replayed(record, confirmedLayer, held.queue);
|
|
3616
3787
|
const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
|
|
@@ -3621,41 +3792,33 @@ export function composeRuntime(options) {
|
|
|
3621
3792
|
key: record.key,
|
|
3622
3793
|
record: storedRecord(record),
|
|
3623
3794
|
roots: record.typedSelection?.roots ?? [],
|
|
3624
|
-
|
|
3795
|
+
composed: composedOf(composed(record)),
|
|
3625
3796
|
overlay: diff.overlay,
|
|
3626
3797
|
},
|
|
3627
3798
|
],
|
|
3628
3799
|
});
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count });
|
|
3800
|
+
const { count, indexed } = committedRoom(room.locator, answer);
|
|
3801
|
+
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
|
|
3632
3802
|
changed = changeSet(room.locator, {
|
|
3633
3803
|
documents: answer.changed[room.locator] ?? [],
|
|
3634
3804
|
room: true,
|
|
3635
3805
|
});
|
|
3636
|
-
if (read === 'staged' || read === 'deferred') {
|
|
3637
|
-
if (declarationRefusals.delete(room.locator)) {
|
|
3638
|
-
sourceRefusals.delete(room.locator);
|
|
3639
|
-
unreadable.delete(room.locator);
|
|
3640
|
-
}
|
|
3641
|
-
}
|
|
3642
|
-
else {
|
|
3643
|
-
refusedSource(room, read);
|
|
3644
|
-
declarationRefusals.add(room.locator);
|
|
3645
|
-
}
|
|
3646
3806
|
}
|
|
3647
|
-
|
|
3648
|
-
if (next.status === 'current') {
|
|
3649
|
-
liveModels.set(key, JSON.stringify(next.snapshot));
|
|
3650
|
-
invalidModels.delete(key);
|
|
3651
|
-
}
|
|
3652
|
-
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
3807
|
+
heldModel(room, next);
|
|
3653
3808
|
if (changed === null)
|
|
3654
3809
|
emitRooms();
|
|
3655
3810
|
else
|
|
3656
3811
|
publishChangeSet(changed);
|
|
3657
3812
|
});
|
|
3658
3813
|
}
|
|
3814
|
+
function heldModel(room, next) {
|
|
3815
|
+
models.set(room.locator, next);
|
|
3816
|
+
if (next.status === 'current') {
|
|
3817
|
+
liveModels.set(room.locator, JSON.stringify(next.snapshot));
|
|
3818
|
+
invalidModels.delete(room.locator);
|
|
3819
|
+
}
|
|
3820
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
3821
|
+
}
|
|
3659
3822
|
async function scoped(result, room, local) {
|
|
3660
3823
|
if (result.scope === 'content' && !result.retryable) {
|
|
3661
3824
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
@@ -3669,14 +3832,14 @@ export function composeRuntime(options) {
|
|
|
3669
3832
|
});
|
|
3670
3833
|
let scopedAway = room;
|
|
3671
3834
|
if (result.scope === 'model')
|
|
3672
|
-
await retainModel(local, room,
|
|
3835
|
+
await retainModel(local, room, {
|
|
3673
3836
|
status: 'unavailable',
|
|
3674
3837
|
reason: 'not-readable',
|
|
3675
3838
|
});
|
|
3676
3839
|
else if (result.scope !== 'operation') {
|
|
3677
3840
|
unreadable.add(room.locator);
|
|
3678
3841
|
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
3679
|
-
await retainModel(local, scopedAway,
|
|
3842
|
+
await retainModel(local, scopedAway, modelOf(room.locator));
|
|
3680
3843
|
}
|
|
3681
3844
|
if (!continuing())
|
|
3682
3845
|
return;
|
|
@@ -3684,6 +3847,17 @@ export function composeRuntime(options) {
|
|
|
3684
3847
|
emitRooms();
|
|
3685
3848
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
3686
3849
|
}
|
|
3850
|
+
function name(locator) {
|
|
3851
|
+
const stream = options.adapter.roomStream;
|
|
3852
|
+
const record = replicas.get(locator)?.record;
|
|
3853
|
+
const room = discovered.get(locator);
|
|
3854
|
+
if (stream === undefined || record === undefined || room === undefined)
|
|
3855
|
+
return;
|
|
3856
|
+
if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
|
|
3857
|
+
return;
|
|
3858
|
+
naming += 1;
|
|
3859
|
+
stream.hold([{ locator, roomVersion: record.roomVersion }]);
|
|
3860
|
+
}
|
|
3687
3861
|
function heldRooms(listed) {
|
|
3688
3862
|
return listed.flatMap((room) => {
|
|
3689
3863
|
const record = replicas.get(room.locator)?.record;
|
|
@@ -3701,14 +3875,19 @@ export function composeRuntime(options) {
|
|
|
3701
3875
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
3702
3876
|
});
|
|
3703
3877
|
if (attempt === 'opened') {
|
|
3878
|
+
naming += 1;
|
|
3704
3879
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
3705
3880
|
handed = true;
|
|
3706
3881
|
}
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3882
|
+
for (const room of ready) {
|
|
3883
|
+
if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
|
|
3884
|
+
if (attempt === 'authorized')
|
|
3885
|
+
name(room.locator);
|
|
3886
|
+
continue;
|
|
3887
|
+
}
|
|
3888
|
+
void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
|
|
3889
|
+
}
|
|
3890
|
+
const failed = prepared.find((room) => room.status === 'rejected');
|
|
3712
3891
|
if (failed !== undefined)
|
|
3713
3892
|
throw failed.reason;
|
|
3714
3893
|
}
|
|
@@ -3726,27 +3905,29 @@ export function composeRuntime(options) {
|
|
|
3726
3905
|
return false;
|
|
3727
3906
|
}
|
|
3728
3907
|
if (prior !== undefined)
|
|
3729
|
-
await retainModel(local, room,
|
|
3908
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3730
3909
|
if (discoveryPaused || authorization === undefined)
|
|
3731
3910
|
return false;
|
|
3732
3911
|
return room.replica.admission === 'admitted';
|
|
3733
3912
|
}
|
|
3734
3913
|
async function caughtUp(room, local, progress, live) {
|
|
3735
|
-
if (
|
|
3914
|
+
if (options.adapter.roomStream !== undefined || authorization === undefined)
|
|
3915
|
+
return;
|
|
3916
|
+
if (!live() || !behind(room))
|
|
3736
3917
|
return;
|
|
3737
3918
|
const withheld = await advance(room, local, authorization, live);
|
|
3738
3919
|
if (withheld !== null)
|
|
3739
3920
|
await withhold(withheld, room, local, progress);
|
|
3740
3921
|
}
|
|
3741
3922
|
function revoked(locator) {
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3745
|
-
});
|
|
3923
|
+
const model = modelOf(locator);
|
|
3924
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3746
3925
|
}
|
|
3747
3926
|
async function loadRoom(room, local, progress, live) {
|
|
3748
3927
|
if (!live() || authorization === undefined)
|
|
3749
3928
|
return;
|
|
3929
|
+
if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
|
|
3930
|
+
return;
|
|
3750
3931
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
3751
3932
|
await reload(room, local, progress, live);
|
|
3752
3933
|
return;
|
|
@@ -3764,21 +3945,53 @@ export function composeRuntime(options) {
|
|
|
3764
3945
|
reloadsDue.delete(room.locator);
|
|
3765
3946
|
if (!live() || authorization === undefined)
|
|
3766
3947
|
return;
|
|
3948
|
+
const from = standingOf(room.locator);
|
|
3767
3949
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
3768
|
-
if (!live())
|
|
3769
|
-
return;
|
|
3770
3950
|
if (bootstrapped.kind !== 'staged') {
|
|
3771
|
-
|
|
3951
|
+
if (live())
|
|
3952
|
+
await withhold(bootstrapped, room, local, progress);
|
|
3772
3953
|
return;
|
|
3773
3954
|
}
|
|
3774
|
-
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
3955
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
|
|
3775
3956
|
await caughtUp(room, local, progress, live);
|
|
3776
3957
|
}
|
|
3777
3958
|
finally {
|
|
3778
3959
|
caughtUpTo(room.locator);
|
|
3779
3960
|
}
|
|
3780
3961
|
}
|
|
3781
|
-
async function
|
|
3962
|
+
async function asked(authorized) {
|
|
3963
|
+
try {
|
|
3964
|
+
return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
|
|
3965
|
+
}
|
|
3966
|
+
catch {
|
|
3967
|
+
return { kind: 'transport-uncertainty' };
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
function malformed(listed) {
|
|
3971
|
+
for (const room of listed) {
|
|
3972
|
+
if (room.operations === undefined ||
|
|
3973
|
+
!['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
3974
|
+
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
3975
|
+
room.replica === undefined ||
|
|
3976
|
+
typeof room.replica.selection !== 'string' ||
|
|
3977
|
+
room.replica.selection.length === 0 ||
|
|
3978
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
3979
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
3980
|
+
return `invalid model or replica declaration ${room.locator}`;
|
|
3981
|
+
}
|
|
3982
|
+
if (!declaredSelection(room))
|
|
3983
|
+
return `invalid typed selection declaration ${room.locator}`;
|
|
3984
|
+
}
|
|
3985
|
+
return null;
|
|
3986
|
+
}
|
|
3987
|
+
async function signIn(local, signedIn) {
|
|
3988
|
+
if (JSON.stringify(signedIn) === JSON.stringify(member))
|
|
3989
|
+
return;
|
|
3990
|
+
await local.fact('member', signedIn);
|
|
3991
|
+
member = signedIn;
|
|
3992
|
+
emitMember();
|
|
3993
|
+
}
|
|
3994
|
+
async function discoverRooms(local, progress, attempt, live, found) {
|
|
3782
3995
|
if (closed || authorization === undefined) {
|
|
3783
3996
|
notifyReady();
|
|
3784
3997
|
return;
|
|
@@ -3786,13 +3999,9 @@ export function composeRuntime(options) {
|
|
|
3786
3999
|
moveTo(transferring(status));
|
|
3787
4000
|
discoveries += 1;
|
|
3788
4001
|
const started = discoveries;
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
}
|
|
3793
|
-
catch {
|
|
3794
|
-
result = { kind: 'transport-uncertainty' };
|
|
3795
|
-
}
|
|
4002
|
+
const result = found ?? (await asked(authorization));
|
|
4003
|
+
if (result.kind === 'discovery')
|
|
4004
|
+
roomsNamed = true;
|
|
3796
4005
|
if (!live()) {
|
|
3797
4006
|
progress.complete = false;
|
|
3798
4007
|
notifyReady();
|
|
@@ -3800,12 +4009,7 @@ export function composeRuntime(options) {
|
|
|
3800
4009
|
}
|
|
3801
4010
|
journal.record(`connection discover ${result.kind}`);
|
|
3802
4011
|
if (result.kind === 'transport-uncertainty') {
|
|
3803
|
-
|
|
3804
|
-
await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
|
|
3805
|
-
if (room.typedSelection !== null) {
|
|
3806
|
-
await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
|
|
3807
|
-
}
|
|
3808
|
-
}
|
|
4012
|
+
await cachedModels(local);
|
|
3809
4013
|
progress.reachable = false;
|
|
3810
4014
|
progress.complete = false;
|
|
3811
4015
|
notifyReady();
|
|
@@ -3826,26 +4030,12 @@ export function composeRuntime(options) {
|
|
|
3826
4030
|
notifyReady();
|
|
3827
4031
|
return;
|
|
3828
4032
|
}
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
room.replica.selection.length === 0 ||
|
|
3836
|
-
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
3837
|
-
!['locked', 'open'].includes(room.replica.offline)) {
|
|
3838
|
-
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
3839
|
-
await refuse('missing-required-guarantee');
|
|
3840
|
-
notifyReady();
|
|
3841
|
-
return;
|
|
3842
|
-
}
|
|
3843
|
-
if (!declaredSelection(room)) {
|
|
3844
|
-
journal.record(`invalid typed selection declaration ${room.locator}`);
|
|
3845
|
-
await refuse('missing-required-guarantee');
|
|
3846
|
-
notifyReady();
|
|
3847
|
-
return;
|
|
3848
|
-
}
|
|
4033
|
+
const invalid = malformed(result.rooms);
|
|
4034
|
+
if (invalid !== null) {
|
|
4035
|
+
journal.record(invalid);
|
|
4036
|
+
await refuse('missing-required-guarantee');
|
|
4037
|
+
notifyReady();
|
|
4038
|
+
return;
|
|
3849
4039
|
}
|
|
3850
4040
|
if (refusal !== null) {
|
|
3851
4041
|
refusal = null;
|
|
@@ -3861,13 +4051,8 @@ export function composeRuntime(options) {
|
|
|
3861
4051
|
await local.fact('providerLimits', limits);
|
|
3862
4052
|
await local.fact('capabilities', capabilities);
|
|
3863
4053
|
}
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
await local.fact('member', signedIn);
|
|
3867
|
-
member = signedIn;
|
|
3868
|
-
emitMember();
|
|
3869
|
-
}
|
|
3870
|
-
await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
|
|
4054
|
+
await signIn(local, result.member ?? null);
|
|
4055
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
3871
4056
|
}
|
|
3872
4057
|
async function ensureAuthorization() {
|
|
3873
4058
|
if (authorization !== undefined)
|
|
@@ -3882,23 +4067,45 @@ export function composeRuntime(options) {
|
|
|
3882
4067
|
}
|
|
3883
4068
|
}
|
|
3884
4069
|
async function admittedRead(work) {
|
|
3885
|
-
|
|
3886
|
-
|
|
4070
|
+
while (roomReads.free === 0) {
|
|
4071
|
+
await new Promise((admit) => roomReads.waiting.push(admit));
|
|
3887
4072
|
}
|
|
3888
|
-
|
|
3889
|
-
while (reads.free === 0)
|
|
3890
|
-
await new Promise((admit) => reads.waiting.push(admit));
|
|
3891
|
-
reads.free -= 1;
|
|
4073
|
+
roomReads.free -= 1;
|
|
3892
4074
|
try {
|
|
3893
4075
|
await work();
|
|
3894
4076
|
}
|
|
3895
4077
|
finally {
|
|
3896
|
-
|
|
3897
|
-
|
|
4078
|
+
roomReads.free += 1;
|
|
4079
|
+
roomReads.waiting.shift()?.();
|
|
3898
4080
|
}
|
|
3899
4081
|
}
|
|
4082
|
+
function inRoom(locator, work) {
|
|
4083
|
+
roomWork += 1;
|
|
4084
|
+
const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
|
|
4085
|
+
try {
|
|
4086
|
+
return await work(continuing);
|
|
4087
|
+
}
|
|
4088
|
+
finally {
|
|
4089
|
+
roomWork -= 1;
|
|
4090
|
+
}
|
|
4091
|
+
});
|
|
4092
|
+
const chain = ran.then(() => undefined, () => undefined);
|
|
4093
|
+
roomLanes.set(locator, chain);
|
|
4094
|
+
working.add(chain);
|
|
4095
|
+
void chain.finally(() => {
|
|
4096
|
+
working.delete(chain);
|
|
4097
|
+
if (roomLanes.get(locator) === chain)
|
|
4098
|
+
roomLanes.delete(locator);
|
|
4099
|
+
});
|
|
4100
|
+
return ran;
|
|
4101
|
+
}
|
|
3900
4102
|
function roomTask(locator, work) {
|
|
3901
|
-
|
|
4103
|
+
if (stopping)
|
|
4104
|
+
return Promise.resolve();
|
|
4105
|
+
return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
|
|
4106
|
+
}
|
|
4107
|
+
function advanced(room, local, authorized) {
|
|
4108
|
+
return inRoom(room.locator, (live) => advance(room, local, authorized, live));
|
|
3902
4109
|
}
|
|
3903
4110
|
function task(lane, work) {
|
|
3904
4111
|
if (stopping)
|
|
@@ -3914,23 +4121,19 @@ export function composeRuntime(options) {
|
|
|
3914
4121
|
void run.finally(() => working.delete(run));
|
|
3915
4122
|
return run;
|
|
3916
4123
|
}
|
|
3917
|
-
function sync(attempt) {
|
|
4124
|
+
function sync(attempt, found) {
|
|
3918
4125
|
return task(OPENING_LANE, async (live) => {
|
|
3919
4126
|
if (!live()) {
|
|
3920
|
-
|
|
4127
|
+
readyFromStore();
|
|
3921
4128
|
return;
|
|
3922
4129
|
}
|
|
3923
4130
|
const progress = { complete: true, reachable: true };
|
|
3924
|
-
|
|
3925
|
-
if (attempt === 'opened')
|
|
3926
|
-
opened = generation;
|
|
3927
|
-
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
4131
|
+
underway = attempt === 'opened' ? progress : null;
|
|
3928
4132
|
if (attempt === 'opened')
|
|
3929
4133
|
streamed = progress;
|
|
3930
4134
|
complete = false;
|
|
3931
4135
|
recorded = false;
|
|
3932
4136
|
moveTo(transferring(status));
|
|
3933
|
-
handed = false;
|
|
3934
4137
|
try {
|
|
3935
4138
|
const local = await openLocal();
|
|
3936
4139
|
if (!live())
|
|
@@ -3938,7 +4141,7 @@ export function composeRuntime(options) {
|
|
|
3938
4141
|
await ensureAuthorization();
|
|
3939
4142
|
if (!live())
|
|
3940
4143
|
return;
|
|
3941
|
-
await discoverRooms(local, progress, attempt, live);
|
|
4144
|
+
await discoverRooms(local, progress, attempt, live, found);
|
|
3942
4145
|
}
|
|
3943
4146
|
catch {
|
|
3944
4147
|
progress.complete = false;
|
|
@@ -3946,7 +4149,6 @@ export function composeRuntime(options) {
|
|
|
3946
4149
|
progress.reachable = false;
|
|
3947
4150
|
}
|
|
3948
4151
|
finally {
|
|
3949
|
-
gate.release();
|
|
3950
4152
|
notifyReady();
|
|
3951
4153
|
}
|
|
3952
4154
|
if (!live())
|
|
@@ -3958,35 +4160,53 @@ export function composeRuntime(options) {
|
|
|
3958
4160
|
if (handed || closed || progress.reachable)
|
|
3959
4161
|
return;
|
|
3960
4162
|
underway = null;
|
|
3961
|
-
opened = null;
|
|
3962
|
-
journal.record('connection reopen');
|
|
3963
4163
|
await rest(progress, live);
|
|
3964
|
-
options.adapter.roomStream?.reopen();
|
|
3965
4164
|
});
|
|
3966
4165
|
}
|
|
3967
|
-
function
|
|
4166
|
+
function resynchronize() {
|
|
3968
4167
|
return task(OPENING_LANE, async (live) => {
|
|
3969
|
-
if (!live() || !
|
|
4168
|
+
if (!live() || !announced)
|
|
3970
4169
|
return;
|
|
3971
4170
|
const progress = { complete: true, reachable: true };
|
|
3972
|
-
const gate = discover();
|
|
3973
4171
|
moveTo(transferring(status));
|
|
3974
4172
|
try {
|
|
3975
4173
|
const local = await openLocal();
|
|
3976
4174
|
if (!live())
|
|
3977
4175
|
return;
|
|
3978
|
-
await
|
|
4176
|
+
await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
|
|
4177
|
+
}
|
|
4178
|
+
catch {
|
|
4179
|
+
progress.complete = false;
|
|
4180
|
+
}
|
|
4181
|
+
if (live())
|
|
4182
|
+
await finishOpening(progress, live);
|
|
4183
|
+
});
|
|
4184
|
+
}
|
|
4185
|
+
function changeAuthority(change) {
|
|
4186
|
+
return task(OPENING_LANE, async (live) => {
|
|
4187
|
+
if (!live() || !announced)
|
|
4188
|
+
return;
|
|
4189
|
+
const progress = { complete: true, reachable: true };
|
|
4190
|
+
moveTo(transferring(status));
|
|
4191
|
+
try {
|
|
4192
|
+
const local = await openLocal();
|
|
3979
4193
|
if (!live())
|
|
3980
4194
|
return;
|
|
3981
|
-
|
|
4195
|
+
const invalid = malformed(change.rooms);
|
|
4196
|
+
if (invalid !== null) {
|
|
4197
|
+
journal.record(invalid);
|
|
4198
|
+
await refuse('missing-required-guarantee');
|
|
4199
|
+
return;
|
|
4200
|
+
}
|
|
4201
|
+
if (change.member !== undefined)
|
|
4202
|
+
await signIn(local, change.member);
|
|
4203
|
+
discoveries += 1;
|
|
4204
|
+
const listed = await applyDiscovery(change.rooms, discoveries, change.left);
|
|
4205
|
+
await synchronize(listed, local, progress, 'authorized', live);
|
|
3982
4206
|
}
|
|
3983
4207
|
catch {
|
|
3984
4208
|
progress.complete = false;
|
|
3985
4209
|
}
|
|
3986
|
-
finally {
|
|
3987
|
-
gate.release();
|
|
3988
|
-
notifyReady();
|
|
3989
|
-
}
|
|
3990
4210
|
if (!live())
|
|
3991
4211
|
return;
|
|
3992
4212
|
if (!progress.complete)
|
|
@@ -3995,13 +4215,15 @@ export function composeRuntime(options) {
|
|
|
3995
4215
|
});
|
|
3996
4216
|
}
|
|
3997
4217
|
function closedSocket(revoked, lost) {
|
|
3998
|
-
|
|
4218
|
+
notifyReady();
|
|
4219
|
+
const opened = announced;
|
|
3999
4220
|
socketOpen = false;
|
|
4221
|
+
announced = false;
|
|
4222
|
+
handed = false;
|
|
4223
|
+
naming = 0;
|
|
4000
4224
|
connectionLost();
|
|
4001
|
-
if (lost)
|
|
4002
|
-
reads.lost();
|
|
4225
|
+
if (lost)
|
|
4003
4226
|
conversations.lost();
|
|
4004
|
-
}
|
|
4005
4227
|
if (revoked) {
|
|
4006
4228
|
void refuse('revoked-authorization');
|
|
4007
4229
|
return;
|
|
@@ -4014,16 +4236,30 @@ export function composeRuntime(options) {
|
|
|
4014
4236
|
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
4015
4237
|
providerUnreachable: status.providerUnreachable || !opened,
|
|
4016
4238
|
});
|
|
4239
|
+
void closing(opened);
|
|
4017
4240
|
}
|
|
4018
|
-
function
|
|
4019
|
-
|
|
4241
|
+
function closing(opened) {
|
|
4242
|
+
return task(OPENING_LANE, async (live) => {
|
|
4243
|
+
if (!live())
|
|
4244
|
+
return;
|
|
4245
|
+
const local = await openLocal();
|
|
4246
|
+
if (!opened && !socketOpen)
|
|
4247
|
+
await cachedModels(local);
|
|
4248
|
+
await rest(null, live);
|
|
4249
|
+
});
|
|
4250
|
+
}
|
|
4251
|
+
function cachedModels(local) {
|
|
4252
|
+
return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
|
|
4253
|
+
}
|
|
4254
|
+
function caughtUpRooms(expired) {
|
|
4255
|
+
const progress = underway;
|
|
4256
|
+
underway = null;
|
|
4257
|
+
naming = Math.max(0, naming - 1);
|
|
4020
4258
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
4021
|
-
|
|
4022
|
-
opened = null;
|
|
4259
|
+
replaced(expired);
|
|
4023
4260
|
const reloading = progress === null ? [] : owedReloads(expired);
|
|
4024
4261
|
const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
|
|
4025
4262
|
return task(OPENING_LANE, async (live) => {
|
|
4026
|
-
underway = null;
|
|
4027
4263
|
if (!live())
|
|
4028
4264
|
return;
|
|
4029
4265
|
if (progress === null) {
|
|
@@ -4031,24 +4267,14 @@ export function composeRuntime(options) {
|
|
|
4031
4267
|
await rest(cycle, live);
|
|
4032
4268
|
return;
|
|
4033
4269
|
}
|
|
4034
|
-
await roomsCarried([...expired, ...reloading]);
|
|
4035
|
-
if (!live())
|
|
4036
|
-
return;
|
|
4037
4270
|
try {
|
|
4038
4271
|
const local = await openLocal();
|
|
4039
|
-
|
|
4272
|
+
await replacing;
|
|
4040
4273
|
if (progress.reachable) {
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
!reloading.includes(room.locator)) {
|
|
4045
|
-
await confirmModels(room, local);
|
|
4046
|
-
}
|
|
4047
|
-
}
|
|
4274
|
+
await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
|
|
4275
|
+
!expired.includes(room.locator) &&
|
|
4276
|
+
!reloading.includes(room.locator)), 'current', local);
|
|
4048
4277
|
}
|
|
4049
|
-
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
4050
|
-
if (failed !== undefined)
|
|
4051
|
-
throw failed.reason;
|
|
4052
4278
|
}
|
|
4053
4279
|
catch {
|
|
4054
4280
|
progress.complete = false;
|
|
@@ -4060,11 +4286,8 @@ export function composeRuntime(options) {
|
|
|
4060
4286
|
}
|
|
4061
4287
|
function owedReloads(expired) {
|
|
4062
4288
|
const reloading = [...discovered.values()]
|
|
4063
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
4064
|
-
!expired.includes(room.locator) &&
|
|
4065
|
-
(owed.has(room.locator) || revoked(room.locator)))
|
|
4289
|
+
.filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
|
|
4066
4290
|
.map((room) => room.locator);
|
|
4067
|
-
owed.clear();
|
|
4068
4291
|
for (const locator of reloading)
|
|
4069
4292
|
catching.add(locator);
|
|
4070
4293
|
liveQueries.recover();
|
|
@@ -4073,24 +4296,43 @@ export function composeRuntime(options) {
|
|
|
4073
4296
|
async function reloadNow(replaced, progress) {
|
|
4074
4297
|
const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
|
|
4075
4298
|
for (const room of reloaded)
|
|
4076
|
-
reloadsDue.
|
|
4299
|
+
reloadsDue.add(room.locator);
|
|
4077
4300
|
try {
|
|
4078
4301
|
const local = await openLocal();
|
|
4079
|
-
|
|
4302
|
+
for (const room of reloaded) {
|
|
4303
|
+
void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
|
|
4304
|
+
}
|
|
4080
4305
|
}
|
|
4081
4306
|
catch {
|
|
4082
4307
|
for (const room of reloaded)
|
|
4083
4308
|
reloadsDue.delete(room.locator);
|
|
4084
4309
|
progress.reachable = false;
|
|
4085
|
-
return [];
|
|
4086
4310
|
}
|
|
4087
4311
|
}
|
|
4088
|
-
async function
|
|
4089
|
-
|
|
4090
|
-
const model = modelOf(room.locator
|
|
4091
|
-
|
|
4092
|
-
|
|
4312
|
+
async function settleModels(held, status, local) {
|
|
4313
|
+
const owed = (room) => {
|
|
4314
|
+
const model = modelOf(room.locator);
|
|
4315
|
+
return model.status !== 'unavailable' && model.status !== status;
|
|
4316
|
+
};
|
|
4317
|
+
const settled = await exclusive(async () => {
|
|
4318
|
+
if (!continuing())
|
|
4319
|
+
return [];
|
|
4320
|
+
const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
|
|
4321
|
+
for (const room of unchanged) {
|
|
4322
|
+
const next = { ...modelOf(room.locator), status };
|
|
4323
|
+
const replica = replicas.get(room.locator);
|
|
4324
|
+
if (replica !== undefined)
|
|
4325
|
+
replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
|
|
4326
|
+
heldModel(room, next);
|
|
4093
4327
|
}
|
|
4328
|
+
if (unchanged.length > 0)
|
|
4329
|
+
emitRooms();
|
|
4330
|
+
return unchanged;
|
|
4331
|
+
});
|
|
4332
|
+
for (const room of held) {
|
|
4333
|
+
if (settled.includes(room) || !owed(room))
|
|
4334
|
+
continue;
|
|
4335
|
+
await retainModel(local, room, { ...modelOf(room.locator), status });
|
|
4094
4336
|
}
|
|
4095
4337
|
}
|
|
4096
4338
|
function submitLater(locator) {
|
|
@@ -4135,14 +4377,10 @@ export function composeRuntime(options) {
|
|
|
4135
4377
|
try {
|
|
4136
4378
|
const local = await openLocal();
|
|
4137
4379
|
await roomTask(locator, async (live) => {
|
|
4138
|
-
|
|
4139
|
-
if (!current() || authorization === undefined)
|
|
4380
|
+
if (!live() || authorization === undefined)
|
|
4140
4381
|
return;
|
|
4141
|
-
const withheld = await
|
|
4142
|
-
|
|
4143
|
-
abandonedOn(signal),
|
|
4144
|
-
]);
|
|
4145
|
-
if (withheld !== null && current())
|
|
4382
|
+
const withheld = await advance(room, local, authorization, live);
|
|
4383
|
+
if (withheld !== null && live())
|
|
4146
4384
|
await withhold(withheld, room, local, progress);
|
|
4147
4385
|
});
|
|
4148
4386
|
}
|
|
@@ -4197,14 +4435,11 @@ export function composeRuntime(options) {
|
|
|
4197
4435
|
progress.complete = false;
|
|
4198
4436
|
return;
|
|
4199
4437
|
}
|
|
4200
|
-
reads.applied();
|
|
4201
4438
|
submitAll();
|
|
4202
4439
|
for (const locator of stale)
|
|
4203
4440
|
if (repairable.has(locator))
|
|
4204
4441
|
repairLater(locator, {});
|
|
4205
4442
|
await conversations.reconcile();
|
|
4206
|
-
if (status.providerUnreachable)
|
|
4207
|
-
conversations.reconnected();
|
|
4208
4443
|
const reading = { complete: true, reachable: true };
|
|
4209
4444
|
await conversations.catchUp(reading);
|
|
4210
4445
|
if (progress.reachable && reading.reachable)
|
|
@@ -4230,7 +4465,7 @@ export function composeRuntime(options) {
|
|
|
4230
4465
|
if (!live())
|
|
4231
4466
|
return;
|
|
4232
4467
|
let lastCompleted = status.lastCompleted;
|
|
4233
|
-
let synced = complete && stale.size === 0 && !pending();
|
|
4468
|
+
let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
|
|
4234
4469
|
if (synced && !recorded) {
|
|
4235
4470
|
const completedAt = new Date().toISOString();
|
|
4236
4471
|
try {
|
|
@@ -4245,74 +4480,69 @@ export function composeRuntime(options) {
|
|
|
4245
4480
|
}
|
|
4246
4481
|
}
|
|
4247
4482
|
moveTo({
|
|
4248
|
-
state: pending()
|
|
4483
|
+
state: pending()
|
|
4484
|
+
? 'pending'
|
|
4485
|
+
: synced && complete && roomWork === 0 && naming === 0
|
|
4486
|
+
? 'synced'
|
|
4487
|
+
: 'syncing',
|
|
4249
4488
|
lastCompleted,
|
|
4250
|
-
providerUnreachable: !progress.reachable,
|
|
4489
|
+
providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
|
|
4251
4490
|
...attention(),
|
|
4252
4491
|
});
|
|
4253
4492
|
}
|
|
4254
|
-
function
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
}
|
|
4262
|
-
function applyRoom(locator, roomVersion) {
|
|
4263
|
-
announced.set(locator, roomVersion);
|
|
4264
|
-
return task(roomLane(locator), async (live) => {
|
|
4265
|
-
if (discovering?.generation === generation)
|
|
4266
|
-
await discovering.done;
|
|
4267
|
-
const saving = answering.get(locator);
|
|
4268
|
-
if (saving?.generation === generation)
|
|
4269
|
-
await saving.answered;
|
|
4270
|
-
if (!live() || authorization === undefined)
|
|
4271
|
-
return;
|
|
4272
|
-
const room = discovered.get(locator);
|
|
4273
|
-
const held = replicas.get(locator);
|
|
4274
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
4275
|
-
return;
|
|
4276
|
-
if (held !== undefined &&
|
|
4277
|
-
(held.record.replica.selection !== room.replica.selection ||
|
|
4278
|
-
options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
|
|
4279
|
-
return;
|
|
4280
|
-
}
|
|
4281
|
-
const during = underway?.generation === generation ? underway.progress : null;
|
|
4282
|
-
const progress = during ?? { complete: true, reachable: true };
|
|
4283
|
-
if (during === null)
|
|
4284
|
-
moveTo(transferring(status));
|
|
4493
|
+
function applyRoom(change) {
|
|
4494
|
+
const { locator } = change;
|
|
4495
|
+
if (stopping)
|
|
4496
|
+
return Promise.resolve();
|
|
4497
|
+
return inRoom(locator, async (live) => {
|
|
4498
|
+
await answering.get(locator)?.answered;
|
|
4499
|
+
const progress = underway ?? { complete: true, reachable: true };
|
|
4285
4500
|
try {
|
|
4286
4501
|
const local = await openLocal();
|
|
4287
|
-
|
|
4288
|
-
if (!live()
|
|
4289
|
-
return;
|
|
4290
|
-
const
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4502
|
+
for (;;) {
|
|
4503
|
+
if (!live())
|
|
4504
|
+
return null;
|
|
4505
|
+
const room = discovered.get(locator);
|
|
4506
|
+
const held = replicas.get(locator);
|
|
4507
|
+
if (room === undefined ||
|
|
4508
|
+
held === undefined ||
|
|
4509
|
+
room.replica.admission !== 'admitted' ||
|
|
4510
|
+
held.record.replica.selection !== room.replica.selection) {
|
|
4511
|
+
return null;
|
|
4512
|
+
}
|
|
4513
|
+
const at = held.record.roomVersion;
|
|
4514
|
+
if (options.adapter.reached(locator, at, change.roomVersion))
|
|
4515
|
+
return progress;
|
|
4516
|
+
if (!options.adapter.reached(locator, at, change.from)) {
|
|
4517
|
+
name(locator);
|
|
4518
|
+
return progress;
|
|
4519
|
+
}
|
|
4520
|
+
if (underway === null)
|
|
4521
|
+
moveTo(transferring(status));
|
|
4522
|
+
const writing = () => live() && !full;
|
|
4523
|
+
const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, () => change.read()));
|
|
4524
|
+
if (refreshed.kind !== 'advanced')
|
|
4525
|
+
return progress;
|
|
4526
|
+
const landing = {
|
|
4527
|
+
kind: 'refreshed',
|
|
4528
|
+
from: refreshed.from,
|
|
4529
|
+
remodelled: refreshed.remodelled,
|
|
4530
|
+
};
|
|
4531
|
+
if (await publish(local, room, refreshed.record, landing, writing)) {
|
|
4532
|
+
stale.delete(locator);
|
|
4533
|
+
await retainModel(local, room, modelOf(locator));
|
|
4534
|
+
return progress;
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4294
4537
|
}
|
|
4295
4538
|
catch {
|
|
4539
|
+
if (!stale.has(locator))
|
|
4540
|
+
name(locator);
|
|
4296
4541
|
stale.add(locator);
|
|
4542
|
+
return progress;
|
|
4297
4543
|
}
|
|
4298
|
-
|
|
4299
|
-
if (during === null)
|
|
4300
|
-
await rest(progress, live);
|
|
4301
|
-
}
|
|
4302
|
-
});
|
|
4544
|
+
}).then((progress) => rest(progress), () => undefined);
|
|
4303
4545
|
}
|
|
4304
|
-
function roomsCarried(replacing) {
|
|
4305
|
-
const replaced = new Set(replacing.map(roomLane));
|
|
4306
|
-
return Promise.all([...lanes]
|
|
4307
|
-
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
4308
|
-
.map(([, held]) => held.chain));
|
|
4309
|
-
}
|
|
4310
|
-
const receipts = {
|
|
4311
|
-
heads: async () => (await openLocal()).receivedHeads(),
|
|
4312
|
-
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
4313
|
-
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
4314
|
-
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
4315
|
-
};
|
|
4316
4546
|
function observeStream() {
|
|
4317
4547
|
const stream = options.adapter.roomStream;
|
|
4318
4548
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -4322,29 +4552,29 @@ export function composeRuntime(options) {
|
|
|
4322
4552
|
return;
|
|
4323
4553
|
if (event.kind === 'opened') {
|
|
4324
4554
|
socketOpen = true;
|
|
4555
|
+
announced = false;
|
|
4556
|
+
handed = false;
|
|
4557
|
+
naming = 0;
|
|
4325
4558
|
connectionLive();
|
|
4326
4559
|
generation += 1;
|
|
4327
|
-
reads.opened();
|
|
4328
4560
|
conversations.reopened();
|
|
4329
4561
|
recovery.resume();
|
|
4330
|
-
|
|
4562
|
+
}
|
|
4563
|
+
else if (event.kind === 'discovered') {
|
|
4564
|
+
if (!socketOpen)
|
|
4565
|
+
return;
|
|
4566
|
+
announced = true;
|
|
4567
|
+
void sync('opened', event.discovery);
|
|
4331
4568
|
}
|
|
4332
4569
|
else if (event.kind === 'closed')
|
|
4333
4570
|
closedSocket(event.revoked, event.lost);
|
|
4334
|
-
else if (event.kind === '
|
|
4335
|
-
void
|
|
4571
|
+
else if (event.kind === 'caught-up')
|
|
4572
|
+
void caughtUpRooms(event.expired);
|
|
4336
4573
|
else if (event.kind === 'authority-changed')
|
|
4337
|
-
void
|
|
4574
|
+
void changeAuthority(event);
|
|
4338
4575
|
else
|
|
4339
|
-
void applyRoom(event
|
|
4340
|
-
}
|
|
4341
|
-
}
|
|
4342
|
-
async function heldRevisions(locator, paths) {
|
|
4343
|
-
const held = replicas.get(locator);
|
|
4344
|
-
if (held === undefined || paths.length === 0)
|
|
4345
|
-
return new Map();
|
|
4346
|
-
const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
|
|
4347
|
-
return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
|
|
4576
|
+
void applyRoom(event);
|
|
4577
|
+
});
|
|
4348
4578
|
}
|
|
4349
4579
|
function writesHeld() {
|
|
4350
4580
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -4414,8 +4644,9 @@ export function composeRuntime(options) {
|
|
|
4414
4644
|
if (closed)
|
|
4415
4645
|
return;
|
|
4416
4646
|
closed = true;
|
|
4417
|
-
|
|
4418
|
-
|
|
4647
|
+
notifyDiscovered();
|
|
4648
|
+
if (probing !== null)
|
|
4649
|
+
clearInterval(probing);
|
|
4419
4650
|
connectionLive();
|
|
4420
4651
|
lockListeners.clear();
|
|
4421
4652
|
readinessWatches.clear();
|
|
@@ -4427,6 +4658,7 @@ export function composeRuntime(options) {
|
|
|
4427
4658
|
conversations.close();
|
|
4428
4659
|
void recovery.retire();
|
|
4429
4660
|
roomListeners.clear();
|
|
4661
|
+
unlistedRooms.clear();
|
|
4430
4662
|
refusalListeners.clear();
|
|
4431
4663
|
storageLossListeners.clear();
|
|
4432
4664
|
statusListeners.clear();
|
|
@@ -4439,6 +4671,7 @@ export function composeRuntime(options) {
|
|
|
4439
4671
|
}
|
|
4440
4672
|
async function stop() {
|
|
4441
4673
|
stopping = true;
|
|
4674
|
+
inFlight.abort();
|
|
4442
4675
|
await recovery.retire();
|
|
4443
4676
|
await Promise.all(working);
|
|
4444
4677
|
await mutations;
|
|
@@ -4452,12 +4685,12 @@ export function composeRuntime(options) {
|
|
|
4452
4685
|
observeStream();
|
|
4453
4686
|
void openLocal().then(() => {
|
|
4454
4687
|
if (capabilities.length > 0)
|
|
4455
|
-
|
|
4688
|
+
readyFromStore();
|
|
4456
4689
|
}, () => {
|
|
4457
|
-
|
|
4690
|
+
readyFromStore();
|
|
4458
4691
|
});
|
|
4459
4692
|
if (options.adapter.roomStream === undefined)
|
|
4460
|
-
void sync('unstreamed');
|
|
4693
|
+
void sync('unstreamed', null);
|
|
4461
4694
|
function planned(input, reach) {
|
|
4462
4695
|
const plan = planOf(input, roomFacts(reach), planReach(reach));
|
|
4463
4696
|
const named = plan.paths !== null &&
|
|
@@ -4602,12 +4835,13 @@ export function composeRuntime(options) {
|
|
|
4602
4835
|
};
|
|
4603
4836
|
}
|
|
4604
4837
|
if (topic === 'readiness') {
|
|
4605
|
-
return watchReadiness({ target: '', code: 'stored',
|
|
4838
|
+
return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
|
|
4606
4839
|
}
|
|
4607
4840
|
if (topic === 'rooms') {
|
|
4608
|
-
|
|
4609
|
-
|
|
4841
|
+
unlistedRooms.add(deliver);
|
|
4842
|
+
emitKnownRooms();
|
|
4610
4843
|
return () => {
|
|
4844
|
+
unlistedRooms.delete(deliver);
|
|
4611
4845
|
roomListeners.delete(deliver);
|
|
4612
4846
|
};
|
|
4613
4847
|
}
|
|
@@ -4661,7 +4895,17 @@ export function composeRuntime(options) {
|
|
|
4661
4895
|
return openLocal().then(() => undefined);
|
|
4662
4896
|
},
|
|
4663
4897
|
rooms() {
|
|
4664
|
-
|
|
4898
|
+
if (closed)
|
|
4899
|
+
return Promise.resolve([]);
|
|
4900
|
+
return new Promise((settle) => {
|
|
4901
|
+
const answer = (value) => {
|
|
4902
|
+
unlistedRooms.delete(answer);
|
|
4903
|
+
roomListeners.delete(answer);
|
|
4904
|
+
settle(value);
|
|
4905
|
+
};
|
|
4906
|
+
unlistedRooms.add(answer);
|
|
4907
|
+
emitKnownRooms();
|
|
4908
|
+
});
|
|
4665
4909
|
},
|
|
4666
4910
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
4667
4911
|
search: (input) => searched(input, null, []),
|
|
@@ -4671,7 +4915,7 @@ export function composeRuntime(options) {
|
|
|
4671
4915
|
typedSelection: rooms.get(locator)?.typedSelection === undefined ||
|
|
4672
4916
|
rooms.get(locator)?.typedSelection === null
|
|
4673
4917
|
? null
|
|
4674
|
-
: modelTypes(
|
|
4918
|
+
: modelTypes(builtIn),
|
|
4675
4919
|
};
|
|
4676
4920
|
},
|
|
4677
4921
|
roomDraft: {
|
|
@@ -4688,33 +4932,17 @@ export function composeRuntime(options) {
|
|
|
4688
4932
|
const record = roomDrafts.get(room)?.get(path);
|
|
4689
4933
|
return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
|
|
4690
4934
|
},
|
|
4691
|
-
write(room, path, revision, input) {
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
const
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
? undefined
|
|
4703
|
-
: await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
|
|
4704
|
-
const record = {
|
|
4705
|
-
locator: room,
|
|
4706
|
-
path,
|
|
4707
|
-
revision: roomDraftRevision(),
|
|
4708
|
-
parts: held(current?.parts, parts, file),
|
|
4709
|
-
returned: current?.returned ?? null,
|
|
4710
|
-
};
|
|
4711
|
-
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
4712
|
-
? []
|
|
4713
|
-
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
4714
|
-
rememberRoomDraft(record);
|
|
4715
|
-
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
4716
|
-
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
4717
|
-
});
|
|
4935
|
+
write(room, path, revision, input, next = roomDraftRevision()) {
|
|
4936
|
+
if (typeof next !== 'string' || next === '')
|
|
4937
|
+
return Promise.reject(runtimeError('room-draft-malformed'));
|
|
4938
|
+
return gathered(`room-draft\u0000${room}\u0000${path}`, { revision, input, next }, (earlier, later) => {
|
|
4939
|
+
if (later.revision !== earlier.next)
|
|
4940
|
+
return null;
|
|
4941
|
+
const parts = gatheredParts(earlier.input, later.input);
|
|
4942
|
+
return parts === null
|
|
4943
|
+
? null
|
|
4944
|
+
: { revision: earlier.revision, input: parts, next: later.next };
|
|
4945
|
+
}, (written) => writtenRoomDraft(room, path, written.revision, written.input, written.next));
|
|
4718
4946
|
},
|
|
4719
4947
|
discard(room, path, revision) {
|
|
4720
4948
|
return exclusive(async () => {
|
|
@@ -4741,13 +4969,13 @@ export function composeRuntime(options) {
|
|
|
4741
4969
|
});
|
|
4742
4970
|
},
|
|
4743
4971
|
write(app, value) {
|
|
4744
|
-
return
|
|
4972
|
+
return exclusive(() => keptPresentation(app, () => structuredClone(value)));
|
|
4745
4973
|
},
|
|
4746
4974
|
edit(app, members) {
|
|
4747
|
-
return
|
|
4748
|
-
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(
|
|
4749
|
-
...Object.entries(
|
|
4750
|
-
]));
|
|
4975
|
+
return gathered(`presentation\u0000${app}`, structuredClone(members), (earlier, later) => ({ ...earlier, ...later }), (edited) => keptPresentation(app, (held) => Object.fromEntries([
|
|
4976
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(edited, member)),
|
|
4977
|
+
...Object.entries(edited).filter(([, value]) => value !== null),
|
|
4978
|
+
])));
|
|
4751
4979
|
},
|
|
4752
4980
|
},
|
|
4753
4981
|
save(room, named, writtenBy) {
|
|
@@ -4766,7 +4994,8 @@ export function composeRuntime(options) {
|
|
|
4766
4994
|
part: readPart,
|
|
4767
4995
|
read: readBinaryPath,
|
|
4768
4996
|
link: linkBinaryPath,
|
|
4769
|
-
|
|
4997
|
+
show: showBinaryPath,
|
|
4998
|
+
attach: attachBinary,
|
|
4770
4999
|
},
|
|
4771
5000
|
async call(name, input) {
|
|
4772
5001
|
await ready;
|
|
@@ -4789,7 +5018,8 @@ export function composeRuntime(options) {
|
|
|
4789
5018
|
journal.record('retry');
|
|
4790
5019
|
unpreparable.clear();
|
|
4791
5020
|
queueReadiness();
|
|
4792
|
-
|
|
5021
|
+
if (full)
|
|
5022
|
+
await probed();
|
|
4793
5023
|
},
|
|
4794
5024
|
observe: observing(watch),
|
|
4795
5025
|
async reauthorize() {
|
|
@@ -4803,7 +5033,7 @@ export function composeRuntime(options) {
|
|
|
4803
5033
|
authorization = undefined;
|
|
4804
5034
|
discoveryPaused = false;
|
|
4805
5035
|
if (options.adapter.roomStream === undefined)
|
|
4806
|
-
await sync('unstreamed');
|
|
5036
|
+
await sync('unstreamed', null);
|
|
4807
5037
|
else
|
|
4808
5038
|
await task(OPENING_LANE, ensureAuthorization);
|
|
4809
5039
|
},
|
|
@@ -4811,7 +5041,6 @@ export function composeRuntime(options) {
|
|
|
4811
5041
|
const reachable = {
|
|
4812
5042
|
md,
|
|
4813
5043
|
watch,
|
|
4814
|
-
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
4815
5044
|
plan: planned,
|
|
4816
5045
|
query: (plan) => rowsQuery(plan),
|
|
4817
5046
|
watchPlan,
|
|
@@ -4821,12 +5050,12 @@ export function composeRuntime(options) {
|
|
|
4821
5050
|
lockedRooms,
|
|
4822
5051
|
watchLocks,
|
|
4823
5052
|
located,
|
|
5053
|
+
room: (locator) => listed().find((one) => one.locator === locator),
|
|
4824
5054
|
};
|
|
4825
5055
|
return {
|
|
4826
5056
|
md,
|
|
4827
5057
|
seed,
|
|
4828
5058
|
stop,
|
|
4829
|
-
lose: loseRooms,
|
|
4830
5059
|
watch,
|
|
4831
5060
|
persisted(answer) {
|
|
4832
5061
|
if (answer === persistence)
|
|
@@ -4835,6 +5064,10 @@ export function composeRuntime(options) {
|
|
|
4835
5064
|
journal.record(`persistence ${answer}`);
|
|
4836
5065
|
queueReadiness();
|
|
4837
5066
|
},
|
|
4838
|
-
within: (reach, port) =>
|
|
5067
|
+
within: (reach, port) => {
|
|
5068
|
+
if (full)
|
|
5069
|
+
void probed();
|
|
5070
|
+
return narrowed(reachable, reach, port);
|
|
5071
|
+
},
|
|
4839
5072
|
};
|
|
4840
5073
|
}
|