@agentshouse/mdruntime 0.1.0-ce15a88ae8478 → 0.1.0-ce5c5bb29be5a
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 +30 -34
- 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 +139 -94
- package/dist/client.d.ts +6 -7
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +54 -7
- 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 +82 -30
- package/dist/connection.d.ts +8 -7
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1047 -915
- package/dist/conversation.d.ts +7 -5
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +166 -41
- 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 -3
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +40 -15
- package/dist/index.d.ts +9 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +2 -1
- package/dist/media.d.ts +8 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +218 -0
- package/dist/memory.d.ts +2 -2
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +46 -20
- 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 +69 -46
- 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 +6 -2
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +61 -27
- 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 +19 -26
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +26 -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 -5
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +2 -2
- 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
1
|
import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
|
|
2
|
-
import { attachedModel,
|
|
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,46 @@ 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 reads = {
|
|
124
|
+
bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
|
|
125
|
+
refresh: (authorized, request) => readUntil(options.adapter.refresh(authorized, request), inFlight.signal),
|
|
126
|
+
reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
|
|
127
|
+
};
|
|
112
128
|
let roomsListed = false;
|
|
113
129
|
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;
|
|
130
|
+
function modelOf(locator) {
|
|
131
|
+
return models.get(locator) ?? NO_MODEL;
|
|
121
132
|
}
|
|
122
|
-
function savedModel(locator
|
|
123
|
-
return invalidModels.has(
|
|
133
|
+
function savedModel(locator) {
|
|
134
|
+
return invalidModels.has(locator)
|
|
124
135
|
? { status: 'unavailable', reason: 'revoked' }
|
|
125
|
-
: modelOf(locator
|
|
136
|
+
: modelOf(locator);
|
|
137
|
+
}
|
|
138
|
+
function composed(record) {
|
|
139
|
+
return compositionOf(record, builtIn);
|
|
126
140
|
}
|
|
127
141
|
function compositionFor(record) {
|
|
128
|
-
return
|
|
129
|
-
...record,
|
|
130
|
-
model: savedModel(record.locator, 'collection'),
|
|
131
|
-
selectionModel: savedModel(record.locator, 'typed-selection'),
|
|
132
|
-
});
|
|
142
|
+
return composed({ ...record, model: savedModel(record.locator) });
|
|
133
143
|
}
|
|
134
144
|
let refusal = null;
|
|
135
145
|
let storageLoss = null;
|
|
@@ -154,14 +164,18 @@ export function composeRuntime(options) {
|
|
|
154
164
|
const stale = new Set();
|
|
155
165
|
let generation = 0;
|
|
156
166
|
let discovering = null;
|
|
157
|
-
let opened =
|
|
167
|
+
let opened = false;
|
|
158
168
|
const owed = new Set();
|
|
159
|
-
const reloadsDue = new
|
|
169
|
+
const reloadsDue = new Set();
|
|
160
170
|
const lanes = new Map();
|
|
161
|
-
|
|
171
|
+
const roomLanes = new Map();
|
|
172
|
+
let roomWork = 0;
|
|
173
|
+
const roomReads = { free: ROOM_READS, waiting: [] };
|
|
162
174
|
const working = new Set();
|
|
163
175
|
let mutations = Promise.resolve();
|
|
176
|
+
let gathering = null;
|
|
164
177
|
const roomListeners = new Set();
|
|
178
|
+
const unlistedRooms = new Set();
|
|
165
179
|
const refusalListeners = new Set();
|
|
166
180
|
const storageLossListeners = new Set();
|
|
167
181
|
const statusListeners = new Set();
|
|
@@ -181,19 +195,53 @@ export function composeRuntime(options) {
|
|
|
181
195
|
const serviceModeListeners = new Set();
|
|
182
196
|
let member = null;
|
|
183
197
|
const memberListeners = new Set();
|
|
184
|
-
const prepared = new Set();
|
|
185
198
|
const unpreparedDemand = new Set();
|
|
186
199
|
const unpreparable = new Set();
|
|
187
200
|
let preparing = false;
|
|
188
201
|
let targets = {};
|
|
189
202
|
let persistence = options.shell.persistence;
|
|
190
|
-
let
|
|
191
|
-
|
|
192
|
-
let
|
|
203
|
+
let full = false;
|
|
204
|
+
let probing = null;
|
|
205
|
+
let testing = null;
|
|
206
|
+
const database = {
|
|
207
|
+
async ask(request) {
|
|
208
|
+
try {
|
|
209
|
+
return await options.database.ask(request);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (refusedForSpace(error))
|
|
213
|
+
filled();
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
};
|
|
193
218
|
const readinessWatches = new Set();
|
|
194
219
|
let readinessQueued = false;
|
|
195
220
|
const catching = new Set();
|
|
196
221
|
const transfers = new FencedMap(isFenced);
|
|
222
|
+
const installs = new Set();
|
|
223
|
+
const arrivals = new Map();
|
|
224
|
+
const streaming = new Set();
|
|
225
|
+
const underived = new Map();
|
|
226
|
+
const unindexed = new Map();
|
|
227
|
+
const startedOver = new Map();
|
|
228
|
+
let arrivalReported = 0;
|
|
229
|
+
let streamsFree = STREAMS_HELD;
|
|
230
|
+
let queuedText = 0;
|
|
231
|
+
const installWaiters = [];
|
|
232
|
+
const stageBudget = {
|
|
233
|
+
bytes: STAGE_FLOOR * 4,
|
|
234
|
+
floor: STAGE_FLOOR,
|
|
235
|
+
ceiling: STAGE_BYTES,
|
|
236
|
+
aim: STAGE_AIM_MS,
|
|
237
|
+
};
|
|
238
|
+
const deriveBudget = {
|
|
239
|
+
bytes: DERIVE_FLOOR * 4,
|
|
240
|
+
floor: DERIVE_FLOOR,
|
|
241
|
+
ceiling: DERIVE_BYTES,
|
|
242
|
+
aim: DERIVE_AIM_MS,
|
|
243
|
+
};
|
|
244
|
+
const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
|
|
197
245
|
const conversations = createConversationStore({
|
|
198
246
|
agents: declaredAgents,
|
|
199
247
|
documents: (declaration, after) => rowsQuery(planOf({
|
|
@@ -214,16 +262,27 @@ export function composeRuntime(options) {
|
|
|
214
262
|
stream: () => options.adapter.conversations,
|
|
215
263
|
publish: publishChangeSet,
|
|
216
264
|
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
265
|
+
listed: () => roomsListed,
|
|
266
|
+
full: () => full,
|
|
217
267
|
closed: () => closed,
|
|
218
268
|
unanswered: resendLater,
|
|
219
269
|
expired: (operation) => pastHorizon(operation),
|
|
220
270
|
});
|
|
221
271
|
const journal = createLog();
|
|
222
|
-
const keptFiles = openedFiles(
|
|
223
|
-
let
|
|
272
|
+
const keptFiles = openedFiles(database, isFenced);
|
|
273
|
+
let readyFromStore = () => { };
|
|
224
274
|
const ready = new Promise((resolve) => {
|
|
225
|
-
|
|
275
|
+
readyFromStore = resolve;
|
|
226
276
|
});
|
|
277
|
+
let notifyDiscovered = () => { };
|
|
278
|
+
const firstDiscovery = new Promise((resolve) => {
|
|
279
|
+
notifyDiscovered = resolve;
|
|
280
|
+
});
|
|
281
|
+
let roomsNamed = false;
|
|
282
|
+
function notifyReady() {
|
|
283
|
+
notifyDiscovered();
|
|
284
|
+
readyFromStore();
|
|
285
|
+
}
|
|
227
286
|
function rememberRoomDraft(record) {
|
|
228
287
|
const room = roomDrafts.get(record.locator) ?? new Map();
|
|
229
288
|
room.set(record.path, record);
|
|
@@ -243,7 +302,7 @@ export function composeRuntime(options) {
|
|
|
243
302
|
return undefined;
|
|
244
303
|
const local = await openLocal();
|
|
245
304
|
const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
|
|
246
|
-
return { ...slice, collection: collectionOf(slice.documents,
|
|
305
|
+
return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
|
|
247
306
|
}
|
|
248
307
|
function roomDraftOf(record, visible) {
|
|
249
308
|
return structuredClone({
|
|
@@ -299,14 +358,40 @@ export function composeRuntime(options) {
|
|
|
299
358
|
const value = presentations.get(app);
|
|
300
359
|
return value === undefined ? null : structuredClone(value);
|
|
301
360
|
}
|
|
302
|
-
function
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
361
|
+
async function writtenRoomDraft(room, path, revision, input, next) {
|
|
362
|
+
const parts = readParts(input);
|
|
363
|
+
if (path === '' || normalize(path) !== path)
|
|
364
|
+
throw runtimeError('room-draft-malformed');
|
|
365
|
+
const local = await openLocal();
|
|
366
|
+
const current = roomDrafts.get(room)?.get(path);
|
|
367
|
+
if ((current?.revision ?? null) !== revision) {
|
|
368
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
369
|
+
}
|
|
370
|
+
const file = parts.file === undefined
|
|
371
|
+
? undefined
|
|
372
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined
|
|
373
|
+
? binaryBase(await visibleOf(room, [path]), path)
|
|
374
|
+
: parts.file.base);
|
|
375
|
+
const record = {
|
|
376
|
+
locator: room,
|
|
377
|
+
path,
|
|
378
|
+
revision: next,
|
|
379
|
+
parts: held(current?.parts, parts, file),
|
|
380
|
+
returned: current?.returned ?? null,
|
|
381
|
+
};
|
|
382
|
+
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
383
|
+
? []
|
|
384
|
+
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
385
|
+
rememberRoomDraft(record);
|
|
386
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
387
|
+
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
388
|
+
}
|
|
389
|
+
async function keptPresentation(app, next) {
|
|
390
|
+
const local = await openLocal();
|
|
391
|
+
const stored = next(presentations.get(app));
|
|
392
|
+
await local.present({ app, value: stored });
|
|
393
|
+
presentations.set(app, stored);
|
|
394
|
+
publishChangeSet(changeSet('', { presentation: app }));
|
|
310
395
|
}
|
|
311
396
|
function emitPresentation(app) {
|
|
312
397
|
if (closed)
|
|
@@ -321,6 +406,20 @@ export function composeRuntime(options) {
|
|
|
321
406
|
(queuesWithoutReplica.size > 0 ||
|
|
322
407
|
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
323
408
|
}
|
|
409
|
+
function unsent() {
|
|
410
|
+
const changed = new Map();
|
|
411
|
+
for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
412
|
+
for (const { batch, state } of entriesOf(room)) {
|
|
413
|
+
if (state === 'local')
|
|
414
|
+
continue;
|
|
415
|
+
for (const { path } of batch.changes)
|
|
416
|
+
changed.set(`${room}\u0000${path}`, { room, path });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return [...changed]
|
|
420
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
421
|
+
.map(([, change]) => change);
|
|
422
|
+
}
|
|
324
423
|
function attention() {
|
|
325
424
|
const attentionRequired = [];
|
|
326
425
|
for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
|
|
@@ -332,16 +431,9 @@ export function composeRuntime(options) {
|
|
|
332
431
|
returned: structuredClone(record.returned),
|
|
333
432
|
});
|
|
334
433
|
}
|
|
335
|
-
for (const [room, refusal] of sourceRefusals)
|
|
336
|
-
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
337
434
|
attentionRequired.sort((left, right) => {
|
|
338
435
|
if (left.room !== right.room)
|
|
339
436
|
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
437
|
if (left.path !== right.path)
|
|
346
438
|
return left.path < right.path ? -1 : 1;
|
|
347
439
|
return 0;
|
|
@@ -353,7 +445,7 @@ export function composeRuntime(options) {
|
|
|
353
445
|
if (reset === null || submission.reset < reset)
|
|
354
446
|
reset = submission.reset;
|
|
355
447
|
}
|
|
356
|
-
return { attentionRequired, reset };
|
|
448
|
+
return { attentionRequired, unsent: unsent(), reset };
|
|
357
449
|
}
|
|
358
450
|
function documentsIn(locator) {
|
|
359
451
|
return replicas.get(locator)?.count ?? 0;
|
|
@@ -426,7 +518,7 @@ export function composeRuntime(options) {
|
|
|
426
518
|
const governed = governs(reach, room);
|
|
427
519
|
const attached = [
|
|
428
520
|
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
429
|
-
...(governed.selection ? [
|
|
521
|
+
...(governed.selection ? [builtIn] : []),
|
|
430
522
|
];
|
|
431
523
|
return {
|
|
432
524
|
locator: room.locator,
|
|
@@ -442,11 +534,26 @@ export function composeRuntime(options) {
|
|
|
442
534
|
const value = listed();
|
|
443
535
|
for (const listener of roomListeners)
|
|
444
536
|
listener(value);
|
|
537
|
+
emitKnownRooms();
|
|
538
|
+
}
|
|
539
|
+
function emitKnownRooms() {
|
|
540
|
+
conversations.learned();
|
|
541
|
+
if (unlistedRooms.size === 0)
|
|
542
|
+
return;
|
|
543
|
+
if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
|
|
544
|
+
return;
|
|
545
|
+
const value = listed();
|
|
546
|
+
for (const listener of [...unlistedRooms]) {
|
|
547
|
+
unlistedRooms.delete(listener);
|
|
548
|
+
roomListeners.add(listener);
|
|
549
|
+
listener(value);
|
|
550
|
+
}
|
|
445
551
|
}
|
|
446
552
|
function emitRefusal() {
|
|
447
553
|
queueReadiness();
|
|
448
554
|
for (const listener of refusalListeners)
|
|
449
555
|
listener(refusal);
|
|
556
|
+
emitKnownRooms();
|
|
450
557
|
}
|
|
451
558
|
function emitStorageLoss() {
|
|
452
559
|
queueReadiness();
|
|
@@ -467,6 +574,7 @@ export function composeRuntime(options) {
|
|
|
467
574
|
if (closed)
|
|
468
575
|
return;
|
|
469
576
|
queueReadiness();
|
|
577
|
+
emitKnownRooms();
|
|
470
578
|
const next = status.state === 'synced' && !conversations.current()
|
|
471
579
|
? { ...status, state: 'syncing' }
|
|
472
580
|
: status;
|
|
@@ -480,9 +588,6 @@ export function composeRuntime(options) {
|
|
|
480
588
|
if (localSaves)
|
|
481
589
|
return Object.freeze({ batch, state: 'local' });
|
|
482
590
|
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
591
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
487
592
|
}
|
|
488
593
|
function entriesOf(locator) {
|
|
@@ -501,18 +606,25 @@ export function composeRuntime(options) {
|
|
|
501
606
|
function unpublished(error) {
|
|
502
607
|
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
503
608
|
}
|
|
609
|
+
function indexed(locator) {
|
|
610
|
+
return replicas.get(locator)?.indexed === true;
|
|
611
|
+
}
|
|
504
612
|
async function prepareReplica(locator) {
|
|
505
613
|
for (;;) {
|
|
506
614
|
const held = replicas.get(locator);
|
|
507
|
-
if (held === undefined || closed ||
|
|
615
|
+
if (held === undefined || closed || held.indexed)
|
|
508
616
|
return;
|
|
509
617
|
const key = held.record.key;
|
|
510
|
-
const done = await exclusive(async () =>
|
|
511
|
-
|
|
618
|
+
const done = await exclusive(async () => {
|
|
619
|
+
const local = await openLocal();
|
|
620
|
+
return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
|
|
621
|
+
});
|
|
622
|
+
const now = replicas.get(locator);
|
|
623
|
+
if (now?.record.key !== key)
|
|
512
624
|
continue;
|
|
513
|
-
if (!done)
|
|
625
|
+
if (!done.done)
|
|
514
626
|
continue;
|
|
515
|
-
|
|
627
|
+
replicas.set(locator, { ...now, indexed: true });
|
|
516
628
|
publishChangeSet(changeSet(locator, { documents: null }));
|
|
517
629
|
liveQueries.recover();
|
|
518
630
|
queueReadiness();
|
|
@@ -520,7 +632,7 @@ export function composeRuntime(options) {
|
|
|
520
632
|
}
|
|
521
633
|
}
|
|
522
634
|
function unprepared() {
|
|
523
|
-
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !
|
|
635
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
|
|
524
636
|
}
|
|
525
637
|
function unpreparedBy(locator, error) {
|
|
526
638
|
unpreparable.add(locator);
|
|
@@ -549,7 +661,11 @@ export function composeRuntime(options) {
|
|
|
549
661
|
})();
|
|
550
662
|
}
|
|
551
663
|
function roomsOfReach(reach) {
|
|
552
|
-
return new Set([
|
|
664
|
+
return new Set([
|
|
665
|
+
...reach.rooms,
|
|
666
|
+
...reach.documents.map(roomOfKey),
|
|
667
|
+
...reach.areas.flatMap((area) => (area.types.length === 0 ? [] : [area.room])),
|
|
668
|
+
]);
|
|
553
669
|
}
|
|
554
670
|
function reachedRooms(plan) {
|
|
555
671
|
const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
|
|
@@ -581,7 +697,7 @@ export function composeRuntime(options) {
|
|
|
581
697
|
pending.push({ room: locator, awaits: 'acquisition' });
|
|
582
698
|
continue;
|
|
583
699
|
}
|
|
584
|
-
if (!
|
|
700
|
+
if (!replica.indexed) {
|
|
585
701
|
pending.push({ room: locator, awaits: 'preparation' });
|
|
586
702
|
continue;
|
|
587
703
|
}
|
|
@@ -615,8 +731,6 @@ export function composeRuntime(options) {
|
|
|
615
731
|
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
616
732
|
}
|
|
617
733
|
function roomState(locator) {
|
|
618
|
-
if (sourceRefusals.has(locator))
|
|
619
|
-
return 'refused';
|
|
620
734
|
const listedRoom = discovered.get(locator);
|
|
621
735
|
if (roomsListed &&
|
|
622
736
|
(listedRoom === undefined ||
|
|
@@ -625,7 +739,7 @@ export function composeRuntime(options) {
|
|
|
625
739
|
return 'unadmitted';
|
|
626
740
|
if (!replicas.has(locator))
|
|
627
741
|
return 'acquiring';
|
|
628
|
-
if (!
|
|
742
|
+
if (!indexed(locator))
|
|
629
743
|
return 'preparing';
|
|
630
744
|
return 'complete';
|
|
631
745
|
}
|
|
@@ -646,21 +760,47 @@ export function composeRuntime(options) {
|
|
|
646
760
|
? 'connected'
|
|
647
761
|
: 'connecting';
|
|
648
762
|
}
|
|
649
|
-
function
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
763
|
+
function bytesOf(room, underway) {
|
|
764
|
+
const transfer = transfers.get(room);
|
|
765
|
+
if (transfer !== undefined) {
|
|
766
|
+
const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
|
|
767
|
+
return {
|
|
768
|
+
done: transfer.done.bytes - Math.ceil(owed),
|
|
769
|
+
total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
|
|
773
|
+
return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
|
|
774
|
+
}
|
|
775
|
+
function installationOf(coverage) {
|
|
776
|
+
const rooms = { done: 0, total: 0 };
|
|
777
|
+
const bytes = { done: 0, total: 0 };
|
|
778
|
+
let installing = 0;
|
|
779
|
+
let downloading = false;
|
|
780
|
+
for (const { room, state } of coverage) {
|
|
781
|
+
if (state === 'unadmitted')
|
|
782
|
+
continue;
|
|
783
|
+
rooms.total += 1;
|
|
784
|
+
if (state === 'complete')
|
|
785
|
+
rooms.done += 1;
|
|
786
|
+
const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
|
|
787
|
+
if (underway)
|
|
788
|
+
installing += 1;
|
|
789
|
+
if (underway && (streaming.has(room) || !arrivals.has(room)))
|
|
790
|
+
downloading = true;
|
|
791
|
+
const now = bytesOf(room, underway);
|
|
792
|
+
const earlier = underway ? startedOver.get(room) : undefined;
|
|
793
|
+
bytes.done += Math.max(now.done, earlier?.done ?? 0);
|
|
794
|
+
bytes.total += Math.max(now.total, earlier?.total ?? 0);
|
|
795
|
+
}
|
|
796
|
+
const step = installing === 0
|
|
797
|
+
? null
|
|
798
|
+
: downloading
|
|
799
|
+
? 'downloading'
|
|
800
|
+
: bytes.done < bytes.total
|
|
801
|
+
? 'writing'
|
|
802
|
+
: 'replaying';
|
|
803
|
+
return { step, rooms, bytes };
|
|
664
804
|
}
|
|
665
805
|
function readinessFor(port, reach) {
|
|
666
806
|
const required = requiredRooms(reach);
|
|
@@ -682,18 +822,17 @@ export function composeRuntime(options) {
|
|
|
682
822
|
persistence,
|
|
683
823
|
connection: connectionOf(),
|
|
684
824
|
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
? capacity
|
|
688
|
-
: port.capacity,
|
|
689
|
-
refused: refusedIn(required.rooms),
|
|
825
|
+
space: full ? 'full' : port.space,
|
|
826
|
+
installation: installationOf(coverage),
|
|
690
827
|
lost: storageLoss !== null,
|
|
691
|
-
established: targets[port.target]?.established === true
|
|
828
|
+
established: targets[port.target]?.established === true ||
|
|
829
|
+
(usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
|
|
692
830
|
});
|
|
693
831
|
}
|
|
694
832
|
function establish(name) {
|
|
695
833
|
targets = { ...targets, [name]: { established: true } };
|
|
696
834
|
queueReadiness();
|
|
835
|
+
probeEachMinute();
|
|
697
836
|
void durably((local) => local.fact('targets', targets)).catch(() => {
|
|
698
837
|
journal.record(`target record failed ${name}`);
|
|
699
838
|
});
|
|
@@ -731,87 +870,52 @@ export function composeRuntime(options) {
|
|
|
731
870
|
readinessWatches.delete(watch);
|
|
732
871
|
};
|
|
733
872
|
}
|
|
734
|
-
function
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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 ?? '-'}`);
|
|
873
|
+
function filled() {
|
|
874
|
+
if (full || closed)
|
|
875
|
+
return;
|
|
876
|
+
full = true;
|
|
877
|
+
journal.record('space full');
|
|
753
878
|
queueReadiness();
|
|
754
|
-
|
|
879
|
+
probeEachMinute();
|
|
880
|
+
}
|
|
881
|
+
function installedTarget() {
|
|
882
|
+
return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
|
|
883
|
+
Object.values(targets).some((target) => target.established));
|
|
884
|
+
}
|
|
885
|
+
function probeEachMinute() {
|
|
886
|
+
if (!full || probing !== null || closed || !installedTarget())
|
|
755
887
|
return;
|
|
756
|
-
// A
|
|
757
|
-
|
|
758
|
-
void
|
|
888
|
+
// 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.
|
|
889
|
+
probing = setInterval(() => {
|
|
890
|
+
void probed();
|
|
759
891
|
}, 60_000);
|
|
760
892
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
resume();
|
|
893
|
+
function probed() {
|
|
894
|
+
testing ??= database.ask({ kind: 'space.probe' }).then(() => {
|
|
895
|
+
testing = null;
|
|
896
|
+
spaceFreed();
|
|
897
|
+
return true;
|
|
898
|
+
}, () => {
|
|
899
|
+
testing = null;
|
|
900
|
+
return false;
|
|
901
|
+
});
|
|
902
|
+
return testing;
|
|
772
903
|
}
|
|
773
|
-
function
|
|
774
|
-
if (
|
|
775
|
-
clearInterval(rechecking);
|
|
776
|
-
rechecking = null;
|
|
777
|
-
if (paused.size === 0 && capacity === null)
|
|
904
|
+
function spaceFreed() {
|
|
905
|
+
if (!full)
|
|
778
906
|
return;
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
907
|
+
full = false;
|
|
908
|
+
if (probing !== null)
|
|
909
|
+
clearInterval(probing);
|
|
910
|
+
probing = null;
|
|
911
|
+
unpreparable.clear();
|
|
912
|
+
journal.record('space freed');
|
|
782
913
|
queueReadiness();
|
|
783
914
|
if (!closed && authorization !== undefined)
|
|
784
915
|
void sync('unstreamed');
|
|
785
916
|
}
|
|
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;
|
|
795
|
-
}
|
|
796
|
-
async function admitted(local, locator, needed) {
|
|
797
|
-
if (paused.has(locator))
|
|
798
|
-
return false;
|
|
799
|
-
let available = await estimated().catch(() => null);
|
|
800
|
-
if (available === null || available >= needed)
|
|
801
|
-
return true;
|
|
802
|
-
if (await reclaimed(local))
|
|
803
|
-
available = await estimated().catch(() => null);
|
|
804
|
-
if (available === null || available >= needed)
|
|
805
|
-
return true;
|
|
806
|
-
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
807
|
-
return false;
|
|
808
|
-
}
|
|
809
|
-
async function exceededOn(locator, bytes) {
|
|
810
|
-
const available = await estimated().catch(() => null);
|
|
811
|
-
pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
|
|
812
|
-
}
|
|
813
917
|
function seedRoomHeld(room, version) {
|
|
814
|
-
return
|
|
918
|
+
return indexed(room) && reachedBy(room, version);
|
|
815
919
|
}
|
|
816
920
|
function seedHeld(at) {
|
|
817
921
|
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
@@ -934,6 +1038,10 @@ export function composeRuntime(options) {
|
|
|
934
1038
|
}
|
|
935
1039
|
function publishChangeSet(set) {
|
|
936
1040
|
applyChangeSet(set);
|
|
1041
|
+
for (const { locator, path } of set.documents) {
|
|
1042
|
+
if (path !== null)
|
|
1043
|
+
attachedFiles.get(locator)?.delete(path);
|
|
1044
|
+
}
|
|
937
1045
|
if (set.rooms.length > 0 ||
|
|
938
1046
|
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
939
1047
|
emitRooms();
|
|
@@ -952,6 +1060,8 @@ export function composeRuntime(options) {
|
|
|
952
1060
|
}
|
|
953
1061
|
async function refuse(reason, provider = null) {
|
|
954
1062
|
discoveryPaused = true;
|
|
1063
|
+
inFlight.abort();
|
|
1064
|
+
inFlight = new AbortController();
|
|
955
1065
|
try {
|
|
956
1066
|
if (reason === 'revoked-authorization') {
|
|
957
1067
|
await exclusive(async () => {
|
|
@@ -959,27 +1069,24 @@ export function composeRuntime(options) {
|
|
|
959
1069
|
const records = [...replicas].map(([locator, held]) => ({
|
|
960
1070
|
locator,
|
|
961
1071
|
held,
|
|
962
|
-
record: { ...held.record, model: revoked
|
|
1072
|
+
record: { ...held.record, model: revoked },
|
|
963
1073
|
}));
|
|
1074
|
+
let answer = null;
|
|
964
1075
|
try {
|
|
965
1076
|
const local = await openLocal();
|
|
966
|
-
await local.commit({
|
|
1077
|
+
answer = await local.commit({
|
|
967
1078
|
rooms: records.map(({ locator, record }) => ({
|
|
968
1079
|
locator,
|
|
969
1080
|
key: record.key,
|
|
970
1081
|
record: storedRecord(record),
|
|
971
|
-
|
|
1082
|
+
composed: composedOf(composed(record)),
|
|
972
1083
|
})),
|
|
973
1084
|
});
|
|
974
1085
|
}
|
|
975
1086
|
catch {
|
|
976
1087
|
journal.record('model revocation persistence failed');
|
|
977
1088
|
}
|
|
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
|
-
}
|
|
1089
|
+
const keys = new Set([...models.keys(), ...rooms.keys()]);
|
|
983
1090
|
for (const key of keys) {
|
|
984
1091
|
invalidModels.add(key);
|
|
985
1092
|
models.set(key, revoked);
|
|
@@ -989,8 +1096,8 @@ export function composeRuntime(options) {
|
|
|
989
1096
|
for (const { locator, held, record } of records) {
|
|
990
1097
|
if (replicas.get(locator) !== held)
|
|
991
1098
|
continue;
|
|
992
|
-
|
|
993
|
-
replicas.set(locator, { ...held, record });
|
|
1099
|
+
const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
|
|
1100
|
+
replicas.set(locator, { ...held, record, indexed });
|
|
994
1101
|
}
|
|
995
1102
|
for (const locator of [...rooms.keys()]) {
|
|
996
1103
|
const room = rooms.get(locator);
|
|
@@ -1034,12 +1141,12 @@ export function composeRuntime(options) {
|
|
|
1034
1141
|
? null
|
|
1035
1142
|
: {
|
|
1036
1143
|
roots: selection.roots,
|
|
1037
|
-
model: modelTypes(
|
|
1144
|
+
model: modelTypes(builtIn),
|
|
1038
1145
|
},
|
|
1039
1146
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1040
1147
|
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
1041
1148
|
synchronizationAvailable,
|
|
1042
|
-
refusal: scopedRefusals.get(room.locator) ??
|
|
1149
|
+
refusal: scopedRefusals.get(room.locator) ?? null,
|
|
1043
1150
|
});
|
|
1044
1151
|
}
|
|
1045
1152
|
async function applyDiscovery(found, started, covered) {
|
|
@@ -1123,7 +1230,9 @@ export function composeRuntime(options) {
|
|
|
1123
1230
|
queuesWithoutReplica.delete(locator);
|
|
1124
1231
|
rooms.delete(locator);
|
|
1125
1232
|
roomDrafts.delete(locator);
|
|
1126
|
-
|
|
1233
|
+
attachedFiles.delete(locator);
|
|
1234
|
+
untransferred(locator);
|
|
1235
|
+
startedOver.delete(locator);
|
|
1127
1236
|
catching.delete(locator);
|
|
1128
1237
|
discovered.delete(locator);
|
|
1129
1238
|
announced.delete(locator);
|
|
@@ -1131,17 +1240,10 @@ export function composeRuntime(options) {
|
|
|
1131
1240
|
stale.delete(locator);
|
|
1132
1241
|
repairable.delete(locator);
|
|
1133
1242
|
unpreparable.delete(locator);
|
|
1134
|
-
paused.delete(locator);
|
|
1135
|
-
prepared.delete(locator);
|
|
1136
1243
|
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
|
-
}
|
|
1244
|
+
models.delete(locator);
|
|
1245
|
+
liveModels.delete(locator);
|
|
1246
|
+
invalidModels.delete(locator);
|
|
1145
1247
|
journal.record(`room lost ${locator}`);
|
|
1146
1248
|
}
|
|
1147
1249
|
if (fresh.length === 0)
|
|
@@ -1189,7 +1291,14 @@ export function composeRuntime(options) {
|
|
|
1189
1291
|
});
|
|
1190
1292
|
await eraseFenced(fresh);
|
|
1191
1293
|
}
|
|
1192
|
-
function
|
|
1294
|
+
function retyped(persisted) {
|
|
1295
|
+
if (persisted.status === 'unavailable')
|
|
1296
|
+
return persisted;
|
|
1297
|
+
const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
|
|
1298
|
+
return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
|
|
1299
|
+
}
|
|
1300
|
+
function restored(key, held) {
|
|
1301
|
+
const persisted = retyped(held);
|
|
1193
1302
|
const prior = models.get(key);
|
|
1194
1303
|
if (prior !== undefined &&
|
|
1195
1304
|
prior.status !== 'unavailable' &&
|
|
@@ -1220,8 +1329,9 @@ export function composeRuntime(options) {
|
|
|
1220
1329
|
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1221
1330
|
targets = held.targets;
|
|
1222
1331
|
transfers.clear();
|
|
1223
|
-
|
|
1224
|
-
|
|
1332
|
+
arrivals.clear();
|
|
1333
|
+
streaming.clear();
|
|
1334
|
+
startedOver.clear();
|
|
1225
1335
|
limits = held.providerLimits;
|
|
1226
1336
|
capabilities = held.capabilities;
|
|
1227
1337
|
member = held.member;
|
|
@@ -1231,7 +1341,7 @@ export function composeRuntime(options) {
|
|
|
1231
1341
|
presentations.clear();
|
|
1232
1342
|
for (const record of held.presentation)
|
|
1233
1343
|
presentations.set(record.app, record.value);
|
|
1234
|
-
conversations.hydrate(heldConversations, chatDrafts);
|
|
1344
|
+
conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
|
|
1235
1345
|
pendings.clear();
|
|
1236
1346
|
for (const queue of queues) {
|
|
1237
1347
|
for (const { batch, state } of queue.pending) {
|
|
@@ -1239,7 +1349,7 @@ export function composeRuntime(options) {
|
|
|
1239
1349
|
}
|
|
1240
1350
|
}
|
|
1241
1351
|
replicas.clear();
|
|
1242
|
-
|
|
1352
|
+
reopened.clear();
|
|
1243
1353
|
queuesWithoutReplica.clear();
|
|
1244
1354
|
for (const [locator, queue] of queuedBatches) {
|
|
1245
1355
|
if (!held.rooms.has(locator))
|
|
@@ -1248,39 +1358,40 @@ export function composeRuntime(options) {
|
|
|
1248
1358
|
records.forEach((stored, index) => {
|
|
1249
1359
|
if (fences.has(stored.locator))
|
|
1250
1360
|
return;
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
const record = { ...stored, model, selectionModel };
|
|
1361
|
+
const record = { ...stored, model: restored(stored.locator, stored.model) };
|
|
1362
|
+
reopened.add(record.locator);
|
|
1254
1363
|
replicas.set(record.locator, {
|
|
1255
1364
|
record,
|
|
1256
1365
|
queue: queuedBatches.get(record.locator) ?? [],
|
|
1257
1366
|
overlaid: held.overlaid.get(record.key) ?? new Set(),
|
|
1258
1367
|
count: counts[index] ?? 0,
|
|
1259
1368
|
confirmed: 0,
|
|
1369
|
+
indexed: !held.unindexed.has(record.key),
|
|
1260
1370
|
});
|
|
1261
|
-
if (!held.unprepared.has(record.key))
|
|
1262
|
-
prepared.add(record.locator);
|
|
1263
1371
|
const existing = rooms.get(record.locator);
|
|
1264
1372
|
if (existing === undefined)
|
|
1265
|
-
list(record,
|
|
1373
|
+
list(record, record.roomVersion !== null);
|
|
1266
1374
|
else {
|
|
1267
1375
|
rooms.set(record.locator, {
|
|
1268
1376
|
...existing,
|
|
1269
1377
|
roomVersion: record.roomVersion,
|
|
1270
1378
|
operations: record.operations,
|
|
1271
1379
|
replica: record.replica,
|
|
1272
|
-
model: modelTypes(model),
|
|
1380
|
+
model: modelTypes(record.model),
|
|
1273
1381
|
typedSelection: record.typedSelection === null
|
|
1274
1382
|
? null
|
|
1275
|
-
: {
|
|
1276
|
-
roots: record.typedSelection.roots,
|
|
1277
|
-
model: modelTypes(selectionModel),
|
|
1278
|
-
},
|
|
1383
|
+
: { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
|
|
1279
1384
|
});
|
|
1280
1385
|
}
|
|
1281
1386
|
});
|
|
1282
|
-
|
|
1283
|
-
|
|
1387
|
+
try {
|
|
1388
|
+
for (const key of held.orphans)
|
|
1389
|
+
await opened.release(key);
|
|
1390
|
+
}
|
|
1391
|
+
catch (error) {
|
|
1392
|
+
if (!refusedForSpace(error))
|
|
1393
|
+
throw error;
|
|
1394
|
+
}
|
|
1284
1395
|
if (held.erasing || held.orphans.length > 0)
|
|
1285
1396
|
queueMicrotask(sweep);
|
|
1286
1397
|
return {
|
|
@@ -1305,7 +1416,7 @@ export function composeRuntime(options) {
|
|
|
1305
1416
|
async function open() {
|
|
1306
1417
|
if (closed)
|
|
1307
1418
|
throw new Error('runtime closed');
|
|
1308
|
-
const opened = openRuntimeStore(isFenced,
|
|
1419
|
+
const opened = openRuntimeStore(isFenced, database);
|
|
1309
1420
|
const interrupted = await opened.fences();
|
|
1310
1421
|
for (const locator of interrupted) {
|
|
1311
1422
|
fences.set(locator, { at: discoveries, erased: false });
|
|
@@ -1351,10 +1462,29 @@ export function composeRuntime(options) {
|
|
|
1351
1462
|
return opening;
|
|
1352
1463
|
}
|
|
1353
1464
|
function exclusive(work) {
|
|
1465
|
+
gathering = null;
|
|
1354
1466
|
const next = mutations.then(work, work);
|
|
1355
1467
|
mutations = next.then(() => undefined, () => undefined);
|
|
1356
1468
|
return next;
|
|
1357
1469
|
}
|
|
1470
|
+
function gathered(key, input, merge, work) {
|
|
1471
|
+
const tail = gathering;
|
|
1472
|
+
if (tail !== null && tail.key === key) {
|
|
1473
|
+
const merged = merge(tail.input, input);
|
|
1474
|
+
if (merged !== null) {
|
|
1475
|
+
tail.input = merged;
|
|
1476
|
+
return tail.done;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
const entry = { key, input, done: Promise.resolve() };
|
|
1480
|
+
entry.done = exclusive(() => {
|
|
1481
|
+
if (gathering === entry)
|
|
1482
|
+
gathering = null;
|
|
1483
|
+
return work(entry.input);
|
|
1484
|
+
});
|
|
1485
|
+
gathering = entry;
|
|
1486
|
+
return entry.done;
|
|
1487
|
+
}
|
|
1358
1488
|
function withdrawable(locator) {
|
|
1359
1489
|
return (batch) => {
|
|
1360
1490
|
const state = pendings.get(pendingKey(locator, batch.id));
|
|
@@ -1436,26 +1566,14 @@ export function composeRuntime(options) {
|
|
|
1436
1566
|
},
|
|
1437
1567
|
};
|
|
1438
1568
|
}
|
|
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))
|
|
1569
|
+
function committedRoom(locator, answer) {
|
|
1570
|
+
const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
|
|
1571
|
+
if (!indexed)
|
|
1454
1572
|
prepare([locator]);
|
|
1455
|
-
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
|
|
1573
|
+
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
|
|
1456
1574
|
}
|
|
1457
1575
|
function replayed(record, confirmedLayer, queue) {
|
|
1458
|
-
return stepped(replaying(confirmedLayer, queue, record.locator,
|
|
1576
|
+
return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
|
|
1459
1577
|
}
|
|
1460
1578
|
async function overlayDiff(record, before, visible, touched) {
|
|
1461
1579
|
const kept = before !== undefined && before.record.key === record.key
|
|
@@ -1475,8 +1593,6 @@ export function composeRuntime(options) {
|
|
|
1475
1593
|
function unpairedIn(locator, batch) {
|
|
1476
1594
|
if (!queueIn(locator).some((held) => held.id === batch.id))
|
|
1477
1595
|
return false;
|
|
1478
|
-
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
1479
|
-
return false;
|
|
1480
1596
|
return !leavingIn(batch, locator) || withdrawable(locator)(batch);
|
|
1481
1597
|
}
|
|
1482
1598
|
async function together(local, settlements) {
|
|
@@ -1518,7 +1634,7 @@ export function composeRuntime(options) {
|
|
|
1518
1634
|
const confirmedLayer = overrides === undefined
|
|
1519
1635
|
? read
|
|
1520
1636
|
: await stepped(slicingWith(read, overrides.set, overrides.removed));
|
|
1521
|
-
const composition =
|
|
1637
|
+
const composition = composed(record);
|
|
1522
1638
|
const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
|
|
1523
1639
|
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1524
1640
|
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
@@ -1561,6 +1677,7 @@ export function composeRuntime(options) {
|
|
|
1561
1677
|
key: record.key,
|
|
1562
1678
|
record: storedRecord(record),
|
|
1563
1679
|
roots: record.typedSelection?.roots ?? [],
|
|
1680
|
+
composed: composedOf(composition),
|
|
1564
1681
|
...settling.commit,
|
|
1565
1682
|
...(overrides === undefined
|
|
1566
1683
|
? {}
|
|
@@ -1602,14 +1719,14 @@ export function composeRuntime(options) {
|
|
|
1602
1719
|
const confirmedChanged = settling.commit?.published !== undefined ||
|
|
1603
1720
|
settling.commit?.refreshed === true ||
|
|
1604
1721
|
overrides !== undefined;
|
|
1605
|
-
|
|
1606
|
-
const { count } = committedRoom(locator, record, answer);
|
|
1722
|
+
const { count, indexed } = committedRoom(locator, answer);
|
|
1607
1723
|
replicas.set(locator, {
|
|
1608
1724
|
record,
|
|
1609
1725
|
queue: kept,
|
|
1610
1726
|
overlaid: touched,
|
|
1611
1727
|
count,
|
|
1612
1728
|
confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
|
|
1729
|
+
indexed,
|
|
1613
1730
|
});
|
|
1614
1731
|
return mergeChangeSets([
|
|
1615
1732
|
changeSet(locator, {
|
|
@@ -1624,103 +1741,95 @@ export function composeRuntime(options) {
|
|
|
1624
1741
|
};
|
|
1625
1742
|
}
|
|
1626
1743
|
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
|
-
}
|
|
1744
|
+
const model = record.model;
|
|
1745
|
+
models.set(locator, model);
|
|
1746
|
+
invalidModels.delete(locator);
|
|
1747
|
+
if (model.status === 'current')
|
|
1748
|
+
liveModels.set(locator, JSON.stringify(model.snapshot));
|
|
1749
|
+
else if (model.status === 'unavailable')
|
|
1750
|
+
liveModels.delete(locator);
|
|
1639
1751
|
}
|
|
1640
1752
|
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
|
-
};
|
|
1753
|
+
const answer = answers?.collection;
|
|
1754
|
+
if (answer === undefined)
|
|
1755
|
+
return held;
|
|
1756
|
+
const { state, refused } = attachedModel(answer, held.collection, roomTypes);
|
|
1757
|
+
if (refused !== null) {
|
|
1758
|
+
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
1759
|
+
journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
|
|
1760
|
+
}
|
|
1761
|
+
models.set(locator, state);
|
|
1762
|
+
return { collection: state };
|
|
1656
1763
|
}
|
|
1657
|
-
function publish(local, room,
|
|
1658
|
-
|
|
1764
|
+
function publish(local, room, reached, landing, live) {
|
|
1765
|
+
const landed = exclusive(async () => {
|
|
1659
1766
|
if (!live()) {
|
|
1660
1767
|
caughtUpTo(room.locator);
|
|
1661
1768
|
return true;
|
|
1662
1769
|
}
|
|
1663
1770
|
const prior = replicas.get(room.locator);
|
|
1664
|
-
if (landing.
|
|
1665
|
-
(
|
|
1666
|
-
|
|
1771
|
+
if (!stands(room.locator, landing.from)) {
|
|
1772
|
+
if (landing.kind === 'refreshed') {
|
|
1773
|
+
await local.unrefresh(reached.key);
|
|
1774
|
+
return false;
|
|
1775
|
+
}
|
|
1776
|
+
caughtUpTo(room.locator);
|
|
1777
|
+
await abandoned(local, room.locator);
|
|
1667
1778
|
return false;
|
|
1668
1779
|
}
|
|
1780
|
+
const record = landing.kind === 'refreshed' && prior !== undefined
|
|
1781
|
+
? {
|
|
1782
|
+
...prior.record,
|
|
1783
|
+
roomVersion: reached.roomVersion,
|
|
1784
|
+
...(landing.remodelled ? { model: reached.model } : {}),
|
|
1785
|
+
}
|
|
1786
|
+
: reached;
|
|
1669
1787
|
adoptModels(room.locator, record);
|
|
1670
1788
|
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
1789
|
let set;
|
|
1677
1790
|
try {
|
|
1678
|
-
set = await settle(local, room.locator, prior, record, queue
|
|
1791
|
+
set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
|
|
1679
1792
|
? {
|
|
1680
1793
|
room: true,
|
|
1681
1794
|
view: 'refreshed',
|
|
1682
|
-
commit: { refreshed: true
|
|
1795
|
+
commit: { refreshed: true },
|
|
1683
1796
|
}
|
|
1684
1797
|
: {
|
|
1685
1798
|
room: true,
|
|
1686
1799
|
view: landing.refreshed ? 'refreshed' : 'confirmed',
|
|
1687
1800
|
commit: {
|
|
1688
1801
|
published: { from: prior?.record.key ?? null },
|
|
1689
|
-
composition: compositionOf(record),
|
|
1690
1802
|
...(landing.refreshed ? { refreshed: true } : {}),
|
|
1691
1803
|
},
|
|
1692
1804
|
});
|
|
1693
1805
|
}
|
|
1694
1806
|
catch (error) {
|
|
1695
|
-
if (!
|
|
1807
|
+
if (!refusedForSpace(error))
|
|
1696
1808
|
throw error;
|
|
1697
|
-
await exceededOn(room.locator, null);
|
|
1698
1809
|
caughtUpTo(room.locator);
|
|
1699
1810
|
return true;
|
|
1700
1811
|
}
|
|
1701
1812
|
if (landing.kind === 'bootstrapped') {
|
|
1702
|
-
|
|
1813
|
+
untransferred(room.locator);
|
|
1814
|
+
startedOver.delete(room.locator);
|
|
1703
1815
|
catching.delete(room.locator);
|
|
1704
|
-
|
|
1816
|
+
if (prior !== undefined && prior.record.key !== record.key)
|
|
1817
|
+
sweep();
|
|
1705
1818
|
}
|
|
1706
|
-
for (const batch of accepted)
|
|
1707
|
-
pendings.delete(pendingKey(room.locator, batch.id));
|
|
1708
1819
|
stale.delete(room.locator);
|
|
1709
1820
|
unpreparable.delete(room.locator);
|
|
1710
1821
|
if (unreadable.delete(room.locator))
|
|
1711
1822
|
scopedRefusals.delete(room.locator);
|
|
1712
|
-
sourceRefusals.delete(room.locator);
|
|
1713
|
-
declarationRefusals.delete(room.locator);
|
|
1714
1823
|
list(room, true);
|
|
1715
1824
|
publishChangeSet(set);
|
|
1716
1825
|
await unpair(local);
|
|
1717
1826
|
moveTo({ ...status, ...attention() });
|
|
1718
|
-
|
|
1719
|
-
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1827
|
+
if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1720
1828
|
submitLater(room.locator);
|
|
1721
1829
|
}
|
|
1722
1830
|
return true;
|
|
1723
1831
|
});
|
|
1832
|
+
return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
|
|
1724
1833
|
}
|
|
1725
1834
|
async function savedRoom(locator) {
|
|
1726
1835
|
const local = await openLocal();
|
|
@@ -1745,12 +1854,7 @@ export function composeRuntime(options) {
|
|
|
1745
1854
|
return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
|
|
1746
1855
|
}
|
|
1747
1856
|
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
|
-
}
|
|
1857
|
+
const read = readSavedParts(named);
|
|
1754
1858
|
const target = await savedRoom(locator);
|
|
1755
1859
|
const saved = [];
|
|
1756
1860
|
const files = [];
|
|
@@ -1759,7 +1863,7 @@ export function composeRuntime(options) {
|
|
|
1759
1863
|
for (const { path, parts } of read) {
|
|
1760
1864
|
const file = parts.file === undefined
|
|
1761
1865
|
? undefined
|
|
1762
|
-
: await heldFile(parts.file.blob, binaryBase(visible, path));
|
|
1866
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
|
|
1763
1867
|
if (parts.file !== undefined && file !== undefined) {
|
|
1764
1868
|
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1765
1869
|
}
|
|
@@ -1794,13 +1898,14 @@ export function composeRuntime(options) {
|
|
|
1794
1898
|
const referring = removed.length === 0
|
|
1795
1899
|
? []
|
|
1796
1900
|
: (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
|
|
1797
|
-
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
|
|
1901
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
|
|
1798
1902
|
const { visible } = await replayed(record, confirmedLayer, replica.queue);
|
|
1799
|
-
const composition =
|
|
1903
|
+
const composition = composed(record);
|
|
1800
1904
|
return {
|
|
1801
1905
|
record,
|
|
1802
1906
|
confirmed: confirmedLayer,
|
|
1803
1907
|
visible,
|
|
1908
|
+
folders: new Set(await local.folders(record.key, 'visible', paths)),
|
|
1804
1909
|
*visibleOver(queue) {
|
|
1805
1910
|
return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
|
|
1806
1911
|
},
|
|
@@ -1812,13 +1917,7 @@ export function composeRuntime(options) {
|
|
|
1812
1917
|
return false;
|
|
1813
1918
|
const local = await openLocal();
|
|
1814
1919
|
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)));
|
|
1920
|
+
return local.typedWithin(record.key, 'visible', roots, false);
|
|
1822
1921
|
}
|
|
1823
1922
|
function textDiffers(left, right) {
|
|
1824
1923
|
return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
|
|
@@ -1829,18 +1928,6 @@ export function composeRuntime(options) {
|
|
|
1829
1928
|
return undefined;
|
|
1830
1929
|
return entry.binary !== null ? null : entry.content;
|
|
1831
1930
|
}
|
|
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
1931
|
function lookupOf(local, key, view) {
|
|
1845
1932
|
return async (paths) => {
|
|
1846
1933
|
if (paths.length === 0)
|
|
@@ -1857,37 +1944,6 @@ export function composeRuntime(options) {
|
|
|
1857
1944
|
const read = parseFrontmatter(entry.content ?? '');
|
|
1858
1945
|
return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
|
|
1859
1946
|
}
|
|
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
1947
|
function overriddenLookup(lookup, overrides) {
|
|
1892
1948
|
return async (paths) => {
|
|
1893
1949
|
const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
|
|
@@ -1905,9 +1961,8 @@ export function composeRuntime(options) {
|
|
|
1905
1961
|
}
|
|
1906
1962
|
function validatorOf(record, confirmedLayer) {
|
|
1907
1963
|
return async (resulting, touched) => {
|
|
1908
|
-
const composition = compositionFor(record);
|
|
1909
1964
|
const local = await openLocal();
|
|
1910
|
-
const
|
|
1965
|
+
const changes = [];
|
|
1911
1966
|
const overrides = new Map();
|
|
1912
1967
|
const pace = paced();
|
|
1913
1968
|
for (const path of new Set([...confirmedLayer.held, ...touched])) {
|
|
@@ -1918,19 +1973,13 @@ export function composeRuntime(options) {
|
|
|
1918
1973
|
continue;
|
|
1919
1974
|
overrides.set(path, now);
|
|
1920
1975
|
if (textOf(was) !== textOf(now))
|
|
1921
|
-
|
|
1976
|
+
changes.push({ path, was: textOf(was), now: textOf(now) });
|
|
1922
1977
|
}
|
|
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);
|
|
1978
|
+
return validatedNeighbourhood(changes, compositionFor(record), {
|
|
1979
|
+
lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
|
|
1980
|
+
texts: textsOf(local, record.key, 'confirmed'),
|
|
1981
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
1982
|
+
});
|
|
1934
1983
|
};
|
|
1935
1984
|
}
|
|
1936
1985
|
function textsOf(local, key, view) {
|
|
@@ -1954,6 +2003,7 @@ export function composeRuntime(options) {
|
|
|
1954
2003
|
operations: room.operations,
|
|
1955
2004
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1956
2005
|
visible: saving.visible,
|
|
2006
|
+
folders: saving.folders,
|
|
1957
2007
|
queued: queuedDocuments(replica.queue),
|
|
1958
2008
|
unmodeled: await unmodeledIn(record),
|
|
1959
2009
|
validate: validatorOf(record, saving.confirmed),
|
|
@@ -1974,6 +2024,7 @@ export function composeRuntime(options) {
|
|
|
1974
2024
|
locator,
|
|
1975
2025
|
key: saving.record.key,
|
|
1976
2026
|
roots: saving.record.typedSelection?.roots ?? [],
|
|
2027
|
+
composed: composedOf(composed(saving.record)),
|
|
1977
2028
|
overlay: diff.overlay,
|
|
1978
2029
|
});
|
|
1979
2030
|
}
|
|
@@ -2005,7 +2056,7 @@ export function composeRuntime(options) {
|
|
|
2005
2056
|
}
|
|
2006
2057
|
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
2007
2058
|
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,
|
|
2059
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
|
|
2009
2060
|
if (changed.length > 0)
|
|
2010
2061
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2011
2062
|
const target = replicas.get(destination);
|
|
@@ -2117,6 +2168,8 @@ export function composeRuntime(options) {
|
|
|
2117
2168
|
submitLater(locator);
|
|
2118
2169
|
}
|
|
2119
2170
|
async function committed(locator, target, saved, consumed, writtenBy, files = []) {
|
|
2171
|
+
if (full)
|
|
2172
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2120
2173
|
const into = movedInto(locator, saved);
|
|
2121
2174
|
if (into !== null) {
|
|
2122
2175
|
return committedMove(locator, into, target, saved, consumed, writtenBy, files);
|
|
@@ -2128,7 +2181,7 @@ export function composeRuntime(options) {
|
|
|
2128
2181
|
...saved.map(({ path }) => path),
|
|
2129
2182
|
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
2130
2183
|
]);
|
|
2131
|
-
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path,
|
|
2184
|
+
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
|
|
2132
2185
|
if (changed.length > 0)
|
|
2133
2186
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2134
2187
|
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
|
|
@@ -2156,6 +2209,7 @@ export function composeRuntime(options) {
|
|
|
2156
2209
|
providerLimits: limits,
|
|
2157
2210
|
handle: room.handle,
|
|
2158
2211
|
visible: saving.visible,
|
|
2212
|
+
folders: saving.folders,
|
|
2159
2213
|
changes,
|
|
2160
2214
|
bases: parts.flatMap(({ bases }) => bases),
|
|
2161
2215
|
queued: queuedDocuments(replica.queue),
|
|
@@ -2306,6 +2360,9 @@ export function composeRuntime(options) {
|
|
|
2306
2360
|
async function readHistory(locator, path, query = {}) {
|
|
2307
2361
|
if (closed || refusal !== null)
|
|
2308
2362
|
return UNCERTAIN;
|
|
2363
|
+
await firstDiscovery;
|
|
2364
|
+
if (!roomsNamed)
|
|
2365
|
+
return UNCERTAIN;
|
|
2309
2366
|
try {
|
|
2310
2367
|
await ensureAuthorization();
|
|
2311
2368
|
}
|
|
@@ -2395,14 +2452,12 @@ export function composeRuntime(options) {
|
|
|
2395
2452
|
return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
|
|
2396
2453
|
};
|
|
2397
2454
|
}
|
|
2398
|
-
function binaryRoom(request) {
|
|
2399
|
-
if (request.kind === 'path')
|
|
2400
|
-
return request.locator;
|
|
2401
|
-
return options.adapter.referrerScope(request.referrer)?.locator ?? null;
|
|
2402
|
-
}
|
|
2403
2455
|
async function transferBinary(request, transfer, answer) {
|
|
2404
2456
|
if (closed || refusal !== null)
|
|
2405
2457
|
return UNCERTAIN;
|
|
2458
|
+
await firstDiscovery;
|
|
2459
|
+
if (!roomsNamed)
|
|
2460
|
+
return UNCERTAIN;
|
|
2406
2461
|
try {
|
|
2407
2462
|
await ensureAuthorization();
|
|
2408
2463
|
}
|
|
@@ -2412,16 +2467,13 @@ export function composeRuntime(options) {
|
|
|
2412
2467
|
if (closed || authorization === undefined || refusal !== null)
|
|
2413
2468
|
return UNCERTAIN;
|
|
2414
2469
|
const since = fencings;
|
|
2415
|
-
journal.record(request.
|
|
2416
|
-
? `transfer binary ${request.locator} ${request.path}`
|
|
2417
|
-
: `transfer binary ${request.fileRef}`);
|
|
2470
|
+
journal.record(`transfer binary ${request.locator} ${request.path}`);
|
|
2418
2471
|
try {
|
|
2419
2472
|
const result = await transfer(authorization);
|
|
2420
2473
|
const settled = result;
|
|
2421
2474
|
journal.record(`outcome binary ${settled.kind}`);
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
return failure({ kind: 'room-not-readable', locator });
|
|
2475
|
+
if (fencedSince(request.locator, since))
|
|
2476
|
+
return failure({ kind: 'room-not-readable', locator: request.locator });
|
|
2425
2477
|
if (settled.kind === 'scoped-refusal') {
|
|
2426
2478
|
const room = replicas.get(settled.locator)?.record;
|
|
2427
2479
|
if (room !== undefined)
|
|
@@ -2436,23 +2488,27 @@ export function composeRuntime(options) {
|
|
|
2436
2488
|
return UNCERTAIN;
|
|
2437
2489
|
}
|
|
2438
2490
|
}
|
|
2491
|
+
function fetchBinary(request, name) {
|
|
2492
|
+
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
2493
|
+
}
|
|
2439
2494
|
async function readBinary(request, name) {
|
|
2440
2495
|
if (closed)
|
|
2441
2496
|
return UNCERTAIN;
|
|
2442
2497
|
const held = await keptFiles.find(name);
|
|
2443
2498
|
if (held !== null)
|
|
2444
2499
|
return held;
|
|
2445
|
-
return
|
|
2500
|
+
return fetchBinary(request, name);
|
|
2446
2501
|
}
|
|
2447
2502
|
async function binaryPath(locator, path, revision) {
|
|
2448
2503
|
if (revision !== undefined)
|
|
2449
2504
|
return { kind: 'path', locator, path, revision };
|
|
2450
2505
|
const replica = replicas.get(locator);
|
|
2451
|
-
if (replica === undefined)
|
|
2452
|
-
return null;
|
|
2453
2506
|
const local = await openLocal();
|
|
2454
|
-
const [held] = await local.entries(replica.record.key, 'visible', [path]);
|
|
2455
|
-
|
|
2507
|
+
const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
|
|
2508
|
+
const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
|
|
2509
|
+
if (attached !== undefined)
|
|
2510
|
+
return { kind: 'path', locator, path, revision: attached.revision };
|
|
2511
|
+
if (replica === undefined || held?.binary === undefined || held.binary === null)
|
|
2456
2512
|
return null;
|
|
2457
2513
|
if (held.binary.state === 'pending')
|
|
2458
2514
|
return { kind: 'pending', localId: held.binary.localId };
|
|
@@ -2468,7 +2524,7 @@ export function composeRuntime(options) {
|
|
|
2468
2524
|
const holds = (at) => {
|
|
2469
2525
|
const kept = confirmedLayer.binaries[at];
|
|
2470
2526
|
return (kept?.state === 'accepted' &&
|
|
2471
|
-
kept.
|
|
2527
|
+
kept.sha256 === record.sha256 &&
|
|
2472
2528
|
confirmedLayer.revisions[at] === record.revision);
|
|
2473
2529
|
};
|
|
2474
2530
|
let confirmedAt = path;
|
|
@@ -2482,49 +2538,91 @@ export function composeRuntime(options) {
|
|
|
2482
2538
|
locator,
|
|
2483
2539
|
path: confirmedAt,
|
|
2484
2540
|
revision: record.revision,
|
|
2485
|
-
fileRef: record.fileRef,
|
|
2486
2541
|
};
|
|
2487
2542
|
}
|
|
2488
|
-
async function
|
|
2543
|
+
async function askedBinary(locator, path, revision) {
|
|
2489
2544
|
await ready;
|
|
2490
2545
|
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2491
|
-
|
|
2546
|
+
return drafted !== undefined && drafted.localId === revision
|
|
2492
2547
|
? { kind: 'pending', localId: revision }
|
|
2493
|
-
:
|
|
2548
|
+
: binaryPath(locator, path, revision);
|
|
2549
|
+
}
|
|
2550
|
+
function pendingBinary(locator, path, localId) {
|
|
2551
|
+
const facts = queuedFacts(locator, path, localId);
|
|
2552
|
+
return facts === null
|
|
2553
|
+
? absentBinary(path)
|
|
2554
|
+
: {
|
|
2555
|
+
kind: 'binary',
|
|
2556
|
+
file: localId,
|
|
2557
|
+
byteLength: facts.byteLength,
|
|
2558
|
+
mediaType: facts.mediaType,
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
async function readBinaryPath(locator, path, revision) {
|
|
2562
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2494
2563
|
if (asked === null)
|
|
2495
2564
|
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
|
-
}
|
|
2565
|
+
if (asked.kind === 'pending')
|
|
2566
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2507
2567
|
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
2508
2568
|
}
|
|
2569
|
+
async function heldMediaType(locator, path) {
|
|
2570
|
+
const replica = replicas.get(locator);
|
|
2571
|
+
const [held] = replica === undefined
|
|
2572
|
+
? []
|
|
2573
|
+
: await (await openLocal()).entries(replica.record.key, 'visible', [path]);
|
|
2574
|
+
return held === undefined
|
|
2575
|
+
? attachedFiles.get(locator)?.get(path)?.mediaType
|
|
2576
|
+
: held.binary?.mediaType;
|
|
2577
|
+
}
|
|
2578
|
+
function linkOf(asked, held) {
|
|
2579
|
+
return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
|
|
2580
|
+
}
|
|
2509
2581
|
async function linkBinaryPath(locator, path, revision) {
|
|
2510
2582
|
await ready;
|
|
2511
2583
|
const asked = await binaryPath(locator, path, revision);
|
|
2512
2584
|
if (asked === null || asked.kind === 'pending')
|
|
2513
2585
|
return absentBinary(path);
|
|
2514
|
-
return
|
|
2586
|
+
return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
|
|
2515
2587
|
}
|
|
2516
|
-
async function
|
|
2588
|
+
async function showBinaryPath(locator, path, revision) {
|
|
2589
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2590
|
+
if (asked === null)
|
|
2591
|
+
return absentBinary(path);
|
|
2592
|
+
if (asked.kind === 'pending')
|
|
2593
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2594
|
+
if (closed)
|
|
2595
|
+
return UNCERTAIN;
|
|
2596
|
+
const name = { locator, path: asked.path, revision: asked.revision };
|
|
2597
|
+
const kept = await keptFiles.find(name);
|
|
2598
|
+
if (kept !== null)
|
|
2599
|
+
return kept;
|
|
2600
|
+
const mediaType = await heldMediaType(locator, path);
|
|
2601
|
+
if (mediaType?.startsWith('image/') === true)
|
|
2602
|
+
return fetchBinary(asked, name);
|
|
2603
|
+
return linkOf(asked, revision === undefined ? mediaType : undefined);
|
|
2604
|
+
}
|
|
2605
|
+
async function attachBinary(locator, file) {
|
|
2517
2606
|
await ready;
|
|
2518
|
-
|
|
2519
|
-
if (
|
|
2520
|
-
return
|
|
2521
|
-
|
|
2607
|
+
await firstDiscovery;
|
|
2608
|
+
if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
|
|
2609
|
+
return OFFLINE;
|
|
2610
|
+
const request = await attachedFile(locator, file);
|
|
2611
|
+
return transferBinary({ locator, path: file.name }, async (held) => {
|
|
2612
|
+
const result = await options.adapter.attach(held, request);
|
|
2613
|
+
if (result.kind === 'attached') {
|
|
2614
|
+
const landed = attachedFiles.get(locator) ?? new Map();
|
|
2615
|
+
landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
|
|
2616
|
+
attachedFiles.set(locator, landed);
|
|
2617
|
+
}
|
|
2618
|
+
return result;
|
|
2619
|
+
}, attachAnswer);
|
|
2522
2620
|
}
|
|
2523
2621
|
function readPart(file, part) {
|
|
2524
|
-
return
|
|
2622
|
+
return database.ask({ kind: 'file.part', key: file, part });
|
|
2525
2623
|
}
|
|
2526
2624
|
async function cleanUp() {
|
|
2527
|
-
const freed = await
|
|
2625
|
+
const freed = await database.ask({ kind: 'opened.clean' });
|
|
2528
2626
|
return freed + ((await options.shell.cleanUp?.()) ?? 0);
|
|
2529
2627
|
}
|
|
2530
2628
|
function declaredAgents() {
|
|
@@ -2656,169 +2754,225 @@ export function composeRuntime(options) {
|
|
|
2656
2754
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
2657
2755
|
!options.adapter.reached(locator, roomVersion, written));
|
|
2658
2756
|
}
|
|
2659
|
-
function
|
|
2660
|
-
|
|
2661
|
-
const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
|
|
2662
|
-
for (const path of paths) {
|
|
2663
|
-
const entry = overrides.get(path);
|
|
2664
|
-
if (entry === undefined)
|
|
2665
|
-
continue;
|
|
2666
|
-
if (entry === null)
|
|
2667
|
-
known.delete(path);
|
|
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;
|
|
2753
|
-
}
|
|
2754
|
-
function refreshingOf(local, key, liveness) {
|
|
2757
|
+
function refreshingOf(local, key, composition, liveness, replaying = null) {
|
|
2758
|
+
const write = replaying === null ? exclusive : finishingWrite;
|
|
2755
2759
|
return {
|
|
2760
|
+
turn() {
|
|
2761
|
+
if (replaying !== null)
|
|
2762
|
+
installing(replaying);
|
|
2763
|
+
},
|
|
2756
2764
|
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2757
2765
|
async stage(page) {
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
const last =
|
|
2761
|
-
const
|
|
2766
|
+
for (let written = 0;;) {
|
|
2767
|
+
const end = batchEnd(page.entries, written);
|
|
2768
|
+
const last = end === page.entries.length;
|
|
2769
|
+
const entries = page.entries.slice(written, end);
|
|
2770
|
+
const staged = await write(async () => {
|
|
2762
2771
|
if (!liveness())
|
|
2763
2772
|
return false;
|
|
2764
|
-
await local.refresh(key, last
|
|
2773
|
+
await timed(stageBudget, () => local.refresh(key, composition, last
|
|
2765
2774
|
? { ...page, entries }
|
|
2766
|
-
: { entries, removals: [], folders: [], folderRemovals: [] });
|
|
2775
|
+
: { entries, removals: [], folders: [], folderRemovals: [] }));
|
|
2767
2776
|
return true;
|
|
2768
2777
|
});
|
|
2769
2778
|
if (!staged)
|
|
2770
2779
|
return false;
|
|
2780
|
+
if (last)
|
|
2781
|
+
return true;
|
|
2782
|
+
written = end;
|
|
2771
2783
|
}
|
|
2772
|
-
return true;
|
|
2773
2784
|
},
|
|
2774
2785
|
};
|
|
2775
2786
|
}
|
|
2776
|
-
function
|
|
2777
|
-
|
|
2787
|
+
function batchEnd(entries, from) {
|
|
2788
|
+
let end = from;
|
|
2778
2789
|
let spent = 0;
|
|
2779
|
-
|
|
2780
|
-
const size = (
|
|
2781
|
-
if (spent + size >
|
|
2782
|
-
|
|
2783
|
-
spent = 0;
|
|
2784
|
-
}
|
|
2785
|
-
batches.at(-1).push(entry);
|
|
2790
|
+
while (end < entries.length) {
|
|
2791
|
+
const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
|
|
2792
|
+
if (spent + size > stageBudget.bytes && end > from)
|
|
2793
|
+
break;
|
|
2786
2794
|
spent += size;
|
|
2795
|
+
end += 1;
|
|
2796
|
+
}
|
|
2797
|
+
return end;
|
|
2798
|
+
}
|
|
2799
|
+
async function timed(budget, work) {
|
|
2800
|
+
const started = performance.now();
|
|
2801
|
+
const done = await work();
|
|
2802
|
+
const spent = performance.now() - started;
|
|
2803
|
+
if (spent < budget.aim / 2)
|
|
2804
|
+
budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
|
|
2805
|
+
else if (spent > budget.aim * 2) {
|
|
2806
|
+
budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
|
|
2807
|
+
}
|
|
2808
|
+
return done;
|
|
2809
|
+
}
|
|
2810
|
+
function installWrite(work) {
|
|
2811
|
+
return installTurn(installTurns.starting, work);
|
|
2812
|
+
}
|
|
2813
|
+
function finishingWrite(work) {
|
|
2814
|
+
return installTurn(installTurns.finishing, work);
|
|
2815
|
+
}
|
|
2816
|
+
function installTurn(queue, work) {
|
|
2817
|
+
return new Promise((resolve, reject) => {
|
|
2818
|
+
queue.push(() => void exclusive(work)
|
|
2819
|
+
.then(resolve, reject)
|
|
2820
|
+
.finally(() => {
|
|
2821
|
+
installTurns.busy = false;
|
|
2822
|
+
nextInstallTurn();
|
|
2823
|
+
}));
|
|
2824
|
+
nextInstallTurn();
|
|
2825
|
+
});
|
|
2826
|
+
}
|
|
2827
|
+
function nextInstallTurn() {
|
|
2828
|
+
if (installTurns.busy || installTurns.ahead > 0)
|
|
2829
|
+
return;
|
|
2830
|
+
const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
|
|
2831
|
+
if (turn === undefined)
|
|
2832
|
+
return;
|
|
2833
|
+
installTurns.busy = true;
|
|
2834
|
+
turn();
|
|
2835
|
+
}
|
|
2836
|
+
async function ahead(work) {
|
|
2837
|
+
let taken = false;
|
|
2838
|
+
const hold = {
|
|
2839
|
+
take() {
|
|
2840
|
+
if (taken)
|
|
2841
|
+
return;
|
|
2842
|
+
taken = true;
|
|
2843
|
+
installTurns.ahead += 1;
|
|
2844
|
+
},
|
|
2845
|
+
release() {
|
|
2846
|
+
if (!taken)
|
|
2847
|
+
return;
|
|
2848
|
+
taken = false;
|
|
2849
|
+
installTurns.ahead -= 1;
|
|
2850
|
+
resumeInstall();
|
|
2851
|
+
},
|
|
2852
|
+
};
|
|
2853
|
+
hold.take();
|
|
2854
|
+
try {
|
|
2855
|
+
return await work(hold);
|
|
2856
|
+
}
|
|
2857
|
+
finally {
|
|
2858
|
+
hold.release();
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
function resumeInstall() {
|
|
2862
|
+
if (installTurns.ahead > 0)
|
|
2863
|
+
return;
|
|
2864
|
+
void yielded().then(nextInstallTurn);
|
|
2865
|
+
}
|
|
2866
|
+
async function heldStream() {
|
|
2867
|
+
while (streamsFree === 0)
|
|
2868
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
2869
|
+
streamsFree -= 1;
|
|
2870
|
+
return once(() => {
|
|
2871
|
+
streamsFree += 1;
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
async function unqueued() {
|
|
2875
|
+
while (queuedText >= QUEUED_TEXT) {
|
|
2876
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
2787
2877
|
}
|
|
2788
|
-
return batches;
|
|
2789
2878
|
}
|
|
2790
|
-
|
|
2879
|
+
function once(release) {
|
|
2880
|
+
let released = false;
|
|
2881
|
+
return () => {
|
|
2882
|
+
if (released)
|
|
2883
|
+
return;
|
|
2884
|
+
released = true;
|
|
2885
|
+
release();
|
|
2886
|
+
for (const admit of installWaiters.splice(0))
|
|
2887
|
+
admit();
|
|
2888
|
+
};
|
|
2889
|
+
}
|
|
2890
|
+
function installing(locator) {
|
|
2891
|
+
installs.add(locator);
|
|
2892
|
+
queueReadiness();
|
|
2893
|
+
}
|
|
2894
|
+
function untransferred(locator) {
|
|
2895
|
+
transfers.delete(locator);
|
|
2896
|
+
arrivals.delete(locator);
|
|
2897
|
+
streaming.delete(locator);
|
|
2898
|
+
underived.delete(locator);
|
|
2899
|
+
unindexed.delete(locator);
|
|
2900
|
+
}
|
|
2901
|
+
function setBack(locator) {
|
|
2902
|
+
const now = bytesOf(locator, true);
|
|
2903
|
+
const earlier = startedOver.get(locator);
|
|
2904
|
+
startedOver.set(locator, {
|
|
2905
|
+
done: Math.max(now.done, earlier?.done ?? 0),
|
|
2906
|
+
total: Math.max(now.total, earlier?.total ?? 0),
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
function owedIndexes(locator, bytes, text) {
|
|
2910
|
+
if (bytes <= 0 || text <= 0)
|
|
2911
|
+
return;
|
|
2912
|
+
unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
|
|
2913
|
+
}
|
|
2914
|
+
function built(locator, { done, text }) {
|
|
2915
|
+
const owed = unindexed.get(locator) ?? [];
|
|
2916
|
+
let left = text;
|
|
2917
|
+
while (!done && left > 0 && owed.length > 0) {
|
|
2918
|
+
const [first] = owed;
|
|
2919
|
+
if (left >= first.text) {
|
|
2920
|
+
left -= first.text;
|
|
2921
|
+
owed.shift();
|
|
2922
|
+
continue;
|
|
2923
|
+
}
|
|
2924
|
+
owed[0] = {
|
|
2925
|
+
bytes: (first.bytes * (first.text - left)) / first.text,
|
|
2926
|
+
text: first.text - left,
|
|
2927
|
+
};
|
|
2928
|
+
left = 0;
|
|
2929
|
+
}
|
|
2930
|
+
if (done || owed.length === 0)
|
|
2931
|
+
unindexed.delete(locator);
|
|
2932
|
+
queueReadiness();
|
|
2933
|
+
}
|
|
2934
|
+
function installed(locator) {
|
|
2935
|
+
if (installs.delete(locator))
|
|
2936
|
+
queueReadiness();
|
|
2937
|
+
}
|
|
2938
|
+
async function pullRefresh(authorized, held, liveness, hold) {
|
|
2791
2939
|
if (!liveness())
|
|
2792
2940
|
return unconfirmed;
|
|
2793
|
-
const turn = generation;
|
|
2794
2941
|
const { record } = held;
|
|
2795
|
-
const
|
|
2942
|
+
const from = standingOf(record.locator);
|
|
2796
2943
|
const local = await openLocal();
|
|
2797
2944
|
try {
|
|
2798
2945
|
await exclusive(() => local.unrefresh(record.key));
|
|
2799
|
-
const result = await advancement(
|
|
2946
|
+
const result = await advancement({
|
|
2947
|
+
...reads,
|
|
2948
|
+
async refresh(authorization, request) {
|
|
2949
|
+
hold.release();
|
|
2950
|
+
try {
|
|
2951
|
+
return await reads.refresh(authorization, request);
|
|
2952
|
+
}
|
|
2953
|
+
finally {
|
|
2954
|
+
hold.take();
|
|
2955
|
+
}
|
|
2956
|
+
},
|
|
2957
|
+
}, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
|
|
2800
2958
|
if (result.kind !== 'advanced') {
|
|
2801
2959
|
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2802
2960
|
return result;
|
|
2803
2961
|
}
|
|
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
2962
|
journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
|
|
2813
|
-
return {
|
|
2963
|
+
return {
|
|
2964
|
+
kind: 'advanced',
|
|
2965
|
+
record: result.record,
|
|
2966
|
+
from,
|
|
2967
|
+
remodelled: result.record.model !== record.model,
|
|
2968
|
+
};
|
|
2814
2969
|
}
|
|
2815
2970
|
catch (error) {
|
|
2971
|
+
if (refusedForSpace(error))
|
|
2972
|
+
return unconfirmed;
|
|
2816
2973
|
journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
|
|
2817
2974
|
throw error;
|
|
2818
2975
|
}
|
|
2819
|
-
finally {
|
|
2820
|
-
reads.finished(record.locator, turn);
|
|
2821
|
-
}
|
|
2822
2976
|
}
|
|
2823
2977
|
const heldContents = new Map();
|
|
2824
2978
|
function heldContentOf(local, key) {
|
|
@@ -2836,63 +2990,118 @@ export function composeRuntime(options) {
|
|
|
2836
2990
|
heldContents.set(key, made);
|
|
2837
2991
|
return made;
|
|
2838
2992
|
}
|
|
2993
|
+
async function abandoned(local, locator) {
|
|
2994
|
+
await local.abandon(locator);
|
|
2995
|
+
setBack(locator);
|
|
2996
|
+
untransferred(locator);
|
|
2997
|
+
liveQueries.recover();
|
|
2998
|
+
sweep();
|
|
2999
|
+
}
|
|
2839
3000
|
function staging(local, liveness) {
|
|
3001
|
+
const derived = async (locator, key, composition) => {
|
|
3002
|
+
underived.delete(locator);
|
|
3003
|
+
for (;;) {
|
|
3004
|
+
if (!liveness())
|
|
3005
|
+
return false;
|
|
3006
|
+
const done = await finishingWrite(async () => liveness()
|
|
3007
|
+
? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
|
|
3008
|
+
: null);
|
|
3009
|
+
if (done === null)
|
|
3010
|
+
return false;
|
|
3011
|
+
built(locator, done);
|
|
3012
|
+
if (done.done)
|
|
3013
|
+
return true;
|
|
3014
|
+
}
|
|
3015
|
+
};
|
|
2840
3016
|
return {
|
|
2841
3017
|
held: (locator) => transfers.get(locator) ?? null,
|
|
3018
|
+
async turn(locator, step) {
|
|
3019
|
+
installing(locator);
|
|
3020
|
+
return step === 'downloading' ? heldStream() : () => { };
|
|
3021
|
+
},
|
|
3022
|
+
unqueued,
|
|
3023
|
+
queued(text) {
|
|
3024
|
+
queuedText += text;
|
|
3025
|
+
return once(() => {
|
|
3026
|
+
queuedText -= text;
|
|
3027
|
+
});
|
|
3028
|
+
},
|
|
3029
|
+
arriving(locator) {
|
|
3030
|
+
let ended = false;
|
|
3031
|
+
return {
|
|
3032
|
+
at(bytes) {
|
|
3033
|
+
if (ended)
|
|
3034
|
+
return;
|
|
3035
|
+
arrivals.set(locator, bytes);
|
|
3036
|
+
streaming.add(locator);
|
|
3037
|
+
const now = performance.now();
|
|
3038
|
+
if (now - arrivalReported < ARRIVAL_REPORT_MS)
|
|
3039
|
+
return;
|
|
3040
|
+
arrivalReported = now;
|
|
3041
|
+
queueReadiness();
|
|
3042
|
+
},
|
|
3043
|
+
end() {
|
|
3044
|
+
if (ended)
|
|
3045
|
+
return;
|
|
3046
|
+
ended = true;
|
|
3047
|
+
if (streaming.delete(locator))
|
|
3048
|
+
queueReadiness();
|
|
3049
|
+
},
|
|
3050
|
+
};
|
|
3051
|
+
},
|
|
3052
|
+
announced(locator) {
|
|
3053
|
+
const later = announced.get(locator);
|
|
3054
|
+
return later === undefined ? [] : [later];
|
|
3055
|
+
},
|
|
2842
3056
|
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)
|
|
3057
|
+
const { locator } = record;
|
|
3058
|
+
installing(locator);
|
|
3059
|
+
if (!liveness())
|
|
3060
|
+
return null;
|
|
3061
|
+
const key = await installWrite(async () => {
|
|
3062
|
+
if (!liveness())
|
|
2864
3063
|
return null;
|
|
2865
|
-
|
|
2866
|
-
}
|
|
2867
|
-
|
|
3064
|
+
return local.stage(record, composition, page);
|
|
3065
|
+
});
|
|
3066
|
+
if (key === null || !liveness())
|
|
3067
|
+
return null;
|
|
3068
|
+
const arrived = !transfers.has(locator);
|
|
3069
|
+
const prior = transfers.get(locator)?.done.bytes ?? 0;
|
|
3070
|
+
const held = { ...record, key };
|
|
3071
|
+
transfers.set(locator, held);
|
|
3072
|
+
if (arrived)
|
|
3073
|
+
liveQueries.recover();
|
|
3074
|
+
installing(locator);
|
|
2868
3075
|
liveQueries.recover();
|
|
2869
|
-
|
|
3076
|
+
const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
|
|
3077
|
+
owedIndexes(locator, record.done.bytes - prior, text);
|
|
3078
|
+
const owed = text + (underived.get(locator) ?? 0);
|
|
3079
|
+
underived.set(locator, owed);
|
|
3080
|
+
if (owed < deriveBudget.bytes)
|
|
3081
|
+
return held;
|
|
3082
|
+
return (await derived(locator, key, composition)) ? held : null;
|
|
2870
3083
|
},
|
|
2871
|
-
|
|
3084
|
+
derive: derived,
|
|
3085
|
+
async refreshing(locator, key, composition) {
|
|
2872
3086
|
await exclusive(() => local.unrefresh(key));
|
|
2873
|
-
return refreshingOf(local, key, liveness);
|
|
3087
|
+
return refreshingOf(local, key, composition, liveness, locator);
|
|
2874
3088
|
},
|
|
2875
3089
|
abandon: (locator) => exclusive(async () => {
|
|
2876
|
-
if (
|
|
2877
|
-
|
|
2878
|
-
await local.abandon(locator);
|
|
2879
|
-
transfers.delete(locator);
|
|
2880
|
-
liveQueries.recover();
|
|
2881
|
-
sweep();
|
|
3090
|
+
if (liveness())
|
|
3091
|
+
await abandoned(local, locator);
|
|
2882
3092
|
}),
|
|
2883
|
-
setAside:
|
|
3093
|
+
setAside: async (locator) => {
|
|
2884
3094
|
if (!liveness())
|
|
2885
3095
|
return;
|
|
2886
|
-
|
|
2887
|
-
|
|
3096
|
+
setBack(locator);
|
|
3097
|
+
untransferred(locator);
|
|
2888
3098
|
liveQueries.recover();
|
|
2889
|
-
}
|
|
3099
|
+
},
|
|
2890
3100
|
release: (key) => exclusive(async () => {
|
|
2891
3101
|
await local.release(key);
|
|
2892
3102
|
sweep();
|
|
2893
3103
|
}),
|
|
2894
3104
|
content: (key) => heldContentOf(local, key),
|
|
2895
|
-
validate: (record, view) => validatedRoom(local, record, view, null),
|
|
2896
3105
|
};
|
|
2897
3106
|
}
|
|
2898
3107
|
function reusable(room, local) {
|
|
@@ -2904,30 +3113,28 @@ export function composeRuntime(options) {
|
|
|
2904
3113
|
async function pullBootstrap(authorized, room, local, liveness) {
|
|
2905
3114
|
if (!liveness())
|
|
2906
3115
|
return unconfirmed;
|
|
2907
|
-
const turn = generation;
|
|
2908
3116
|
if (replicas.has(room.locator)) {
|
|
2909
3117
|
catching.add(room.locator);
|
|
2910
3118
|
liveQueries.recover();
|
|
2911
3119
|
}
|
|
2912
3120
|
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));
|
|
3121
|
+
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
3122
|
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
2918
3123
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
2919
|
-
if (result.kind !== 'staged')
|
|
3124
|
+
if (result.kind !== 'staged') {
|
|
2920
3125
|
caughtUpTo(room.locator);
|
|
3126
|
+
installed(room.locator);
|
|
3127
|
+
}
|
|
2921
3128
|
return result;
|
|
2922
3129
|
}
|
|
2923
3130
|
catch (error) {
|
|
2924
|
-
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
2925
3131
|
caughtUpTo(room.locator);
|
|
3132
|
+
installed(room.locator);
|
|
3133
|
+
if (refusedForSpace(error))
|
|
3134
|
+
return unconfirmed;
|
|
3135
|
+
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
2926
3136
|
throw error;
|
|
2927
3137
|
}
|
|
2928
|
-
finally {
|
|
2929
|
-
reads.finished(room.locator, turn);
|
|
2930
|
-
}
|
|
2931
3138
|
}
|
|
2932
3139
|
function caughtUpTo(locator) {
|
|
2933
3140
|
if (catching.delete(locator))
|
|
@@ -2938,69 +3145,79 @@ export function composeRuntime(options) {
|
|
|
2938
3145
|
return (held !== undefined &&
|
|
2939
3146
|
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
2940
3147
|
}
|
|
2941
|
-
|
|
2942
|
-
const
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
return
|
|
3148
|
+
function standingOf(locator) {
|
|
3149
|
+
const held = replicas.get(locator);
|
|
3150
|
+
return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
|
|
3151
|
+
}
|
|
3152
|
+
function stands(locator, from) {
|
|
3153
|
+
const now = standingOf(locator);
|
|
3154
|
+
return now.key === from.key && now.confirmed === from.confirmed;
|
|
2948
3155
|
}
|
|
2949
|
-
async function
|
|
2950
|
-
|
|
3156
|
+
async function advance(room, local, authorized, running) {
|
|
3157
|
+
const live = () => running() && !full;
|
|
3158
|
+
if (!live())
|
|
2951
3159
|
return unconfirmed;
|
|
2952
3160
|
const held = replicas.get(room.locator);
|
|
2953
3161
|
if (held !== undefined) {
|
|
2954
|
-
const refreshed = await
|
|
2955
|
-
if (
|
|
2956
|
-
return
|
|
2957
|
-
if (refreshed
|
|
2958
|
-
stale.delete(room.locator);
|
|
2959
|
-
if (sourceRefusals.has(room.locator))
|
|
2960
|
-
return null;
|
|
2961
|
-
if (unreadable.delete(room.locator)) {
|
|
2962
|
-
scopedRefusals.delete(room.locator);
|
|
2963
|
-
list(room, true);
|
|
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')
|
|
3162
|
+
const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold));
|
|
3163
|
+
if (refreshed === 'behind')
|
|
3164
|
+
return advance(room, local, authorized, live);
|
|
3165
|
+
if (refreshed !== 'beyond-horizon')
|
|
2980
3166
|
return refreshed;
|
|
2981
|
-
if (opened
|
|
3167
|
+
if (opened) {
|
|
2982
3168
|
owed.add(room.locator);
|
|
2983
3169
|
liveQueries.recover();
|
|
2984
3170
|
return unconfirmed;
|
|
2985
3171
|
}
|
|
2986
|
-
if (reloadsDue.
|
|
3172
|
+
if (reloadsDue.has(room.locator))
|
|
2987
3173
|
return unconfirmed;
|
|
2988
3174
|
}
|
|
3175
|
+
const from = standingOf(room.locator);
|
|
2989
3176
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
3177
|
+
if (bootstrapped.kind !== 'staged')
|
|
3178
|
+
return live() ? bootstrapped : unconfirmed;
|
|
3179
|
+
const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
|
|
3180
|
+
if (await publish(local, room, bootstrapped.record, landing, live)) {
|
|
3181
|
+
return live() ? null : unconfirmed;
|
|
3182
|
+
}
|
|
3183
|
+
return reachedBy(room.locator, bootstrapped.record.roomVersion)
|
|
3184
|
+
? null
|
|
3185
|
+
: advance(room, local, authorized, live);
|
|
3186
|
+
}
|
|
3187
|
+
async function refreshedRoom(room, local, authorized, held, live, hold) {
|
|
3188
|
+
const refreshed = await pullRefresh(authorized, held, live, hold);
|
|
2990
3189
|
if (!live())
|
|
2991
3190
|
return unconfirmed;
|
|
2992
|
-
if (
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
3191
|
+
if (refreshed.kind === 'still-current') {
|
|
3192
|
+
stale.delete(room.locator);
|
|
3193
|
+
if (unreadable.delete(room.locator)) {
|
|
3194
|
+
scopedRefusals.delete(room.locator);
|
|
3195
|
+
list(room, true);
|
|
3196
|
+
emitRooms();
|
|
3197
|
+
}
|
|
3198
|
+
return null;
|
|
3199
|
+
}
|
|
3200
|
+
if (refreshed.kind === 'advanced') {
|
|
3201
|
+
const landing = {
|
|
3202
|
+
kind: 'refreshed',
|
|
3203
|
+
from: refreshed.from,
|
|
3204
|
+
remodelled: refreshed.remodelled,
|
|
3205
|
+
};
|
|
3206
|
+
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3207
|
+
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3208
|
+
const reached = now !== undefined &&
|
|
3209
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3210
|
+
return reached ? null : 'behind';
|
|
3211
|
+
}
|
|
3212
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3213
|
+
return null;
|
|
3214
|
+
}
|
|
3215
|
+
return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
|
|
2996
3216
|
}
|
|
2997
3217
|
async function withhold(withheld, room, local, progress) {
|
|
2998
3218
|
if (withheld.kind === 'unconfirmed')
|
|
2999
3219
|
return;
|
|
3000
|
-
|
|
3001
|
-
stale.delete(room.locator);
|
|
3002
|
-
else
|
|
3003
|
-
stale.add(room.locator);
|
|
3220
|
+
stale.add(room.locator);
|
|
3004
3221
|
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
3005
3222
|
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
3006
3223
|
repairable.set(room.locator, reset);
|
|
@@ -3022,30 +3239,10 @@ export function composeRuntime(options) {
|
|
|
3022
3239
|
list(room, false);
|
|
3023
3240
|
emitRooms();
|
|
3024
3241
|
}
|
|
3025
|
-
else if (withheld.kind === 'refused') {
|
|
3026
|
-
refusedSource(room, withheld);
|
|
3027
|
-
}
|
|
3028
3242
|
else if (withheld.kind === 'revoked-authorization') {
|
|
3029
3243
|
await refuse('revoked-authorization');
|
|
3030
3244
|
}
|
|
3031
3245
|
}
|
|
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
3246
|
function leaving(batch, result) {
|
|
3050
3247
|
if (result.kind === 'expired')
|
|
3051
3248
|
return { batch, returned: returnedBy('expired') };
|
|
@@ -3067,8 +3264,8 @@ export function composeRuntime(options) {
|
|
|
3067
3264
|
function settling(acceptance) {
|
|
3068
3265
|
return acceptance !== null && 'apply' in acceptance;
|
|
3069
3266
|
}
|
|
3070
|
-
function
|
|
3071
|
-
return settling(acceptance) ?
|
|
3267
|
+
function failureOf(acceptance) {
|
|
3268
|
+
return settling(acceptance) ? null : acceptance;
|
|
3072
3269
|
}
|
|
3073
3270
|
async function acceptance(room, local, submitted, accepted) {
|
|
3074
3271
|
if (!continuing())
|
|
@@ -3091,34 +3288,6 @@ export function composeRuntime(options) {
|
|
|
3091
3288
|
? accepted.roomVersion
|
|
3092
3289
|
: held,
|
|
3093
3290
|
};
|
|
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
3291
|
changes = back;
|
|
3123
3292
|
}
|
|
3124
3293
|
const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
@@ -3128,8 +3297,6 @@ export function composeRuntime(options) {
|
|
|
3128
3297
|
const set = settled.apply(answer);
|
|
3129
3298
|
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3130
3299
|
unreadable.delete(room.locator);
|
|
3131
|
-
sourceRefusals.delete(room.locator);
|
|
3132
|
-
declarationRefusals.delete(room.locator);
|
|
3133
3300
|
list(room, true);
|
|
3134
3301
|
return set;
|
|
3135
3302
|
},
|
|
@@ -3147,13 +3314,9 @@ export function composeRuntime(options) {
|
|
|
3147
3314
|
const failed = await exclusive(async () => {
|
|
3148
3315
|
const adopting = await acceptance(room, local, submitted, accepted);
|
|
3149
3316
|
await adoptedTogether(local, [adopting]);
|
|
3150
|
-
return
|
|
3317
|
+
return failureOf(adopting);
|
|
3151
3318
|
});
|
|
3152
|
-
|
|
3153
|
-
return true;
|
|
3154
|
-
if (failed.kind === 'refused' && continuing())
|
|
3155
|
-
refusedSource(room, failed);
|
|
3156
|
-
return false;
|
|
3319
|
+
return failed === null;
|
|
3157
3320
|
}
|
|
3158
3321
|
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
3159
3322
|
const other = discovered.get(partner);
|
|
@@ -3167,18 +3330,10 @@ export function composeRuntime(options) {
|
|
|
3167
3330
|
...accepted.destination,
|
|
3168
3331
|
});
|
|
3169
3332
|
await adoptedTogether(local, [left, arrived]);
|
|
3170
|
-
|
|
3171
|
-
if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
|
|
3172
|
-
refusedSource(other, refusedArrival);
|
|
3173
|
-
}
|
|
3174
|
-
return refusalOf(left);
|
|
3333
|
+
return failureOf(left);
|
|
3175
3334
|
});
|
|
3176
3335
|
submitLater(partner);
|
|
3177
|
-
|
|
3178
|
-
return true;
|
|
3179
|
-
if (failed.kind === 'refused' && continuing())
|
|
3180
|
-
refusedSource(room, failed);
|
|
3181
|
-
return false;
|
|
3336
|
+
return failed === null;
|
|
3182
3337
|
}
|
|
3183
3338
|
function queueIn(locator) {
|
|
3184
3339
|
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
@@ -3301,8 +3456,6 @@ export function composeRuntime(options) {
|
|
|
3301
3456
|
if (replica === undefined || head === undefined)
|
|
3302
3457
|
return 'done';
|
|
3303
3458
|
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
3304
|
-
if (state?.accepted !== undefined)
|
|
3305
|
-
return 'done';
|
|
3306
3459
|
const partner = partnerOf(head, room.locator);
|
|
3307
3460
|
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3308
3461
|
submitLater(partner);
|
|
@@ -3416,12 +3569,12 @@ export function composeRuntime(options) {
|
|
|
3416
3569
|
}
|
|
3417
3570
|
if (result.kind === 'expired') {
|
|
3418
3571
|
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
3419
|
-
const there = other === undefined ? null : await
|
|
3572
|
+
const there = other === undefined ? null : await advanced(other, local, authorized);
|
|
3420
3573
|
if (other !== undefined && there !== null) {
|
|
3421
3574
|
await withhold(there, other, local, progress);
|
|
3422
3575
|
return 'done';
|
|
3423
3576
|
}
|
|
3424
|
-
const withheld = await
|
|
3577
|
+
const withheld = await advanced(room, local, authorized);
|
|
3425
3578
|
if (withheld !== null) {
|
|
3426
3579
|
await withhold(withheld, room, local, progress);
|
|
3427
3580
|
return 'done';
|
|
@@ -3443,8 +3596,8 @@ export function composeRuntime(options) {
|
|
|
3443
3596
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
3444
3597
|
const left = leaving(head, result);
|
|
3445
3598
|
const refreshed = async () => ({
|
|
3446
|
-
there: other === undefined ? null : await
|
|
3447
|
-
here: await
|
|
3599
|
+
there: other === undefined ? null : await advanced(other, local, authorized),
|
|
3600
|
+
here: await advanced(room, local, authorized),
|
|
3448
3601
|
});
|
|
3449
3602
|
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
3450
3603
|
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
@@ -3515,6 +3668,7 @@ export function composeRuntime(options) {
|
|
|
3515
3668
|
locator: room.locator,
|
|
3516
3669
|
key: before.record.key,
|
|
3517
3670
|
roots: before.record.typedSelection?.roots ?? [],
|
|
3671
|
+
composed: composedOf(composed(before.record)),
|
|
3518
3672
|
overlay: { set: [], cleared: [...before.overlaid] },
|
|
3519
3673
|
},
|
|
3520
3674
|
]),
|
|
@@ -3547,39 +3701,37 @@ export function composeRuntime(options) {
|
|
|
3547
3701
|
if (signal.aborted)
|
|
3548
3702
|
return;
|
|
3549
3703
|
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
|
-
});
|
|
3704
|
+
await roomTask(room.locator, (live) => reload(room, local, progress, live));
|
|
3556
3705
|
if (signal.aborted)
|
|
3557
3706
|
return;
|
|
3558
3707
|
await rest(progress);
|
|
3559
3708
|
}
|
|
3560
3709
|
}
|
|
3561
3710
|
function awaitingAnswer(locator) {
|
|
3711
|
+
answering.get(locator)?.release();
|
|
3562
3712
|
let release = () => { };
|
|
3563
|
-
const
|
|
3713
|
+
const answered = new Promise((resolve) => {
|
|
3564
3714
|
release = resolve;
|
|
3565
3715
|
});
|
|
3566
|
-
|
|
3716
|
+
const awaited = { answered, release };
|
|
3717
|
+
answering.set(locator, awaited);
|
|
3567
3718
|
return () => {
|
|
3568
|
-
if (answering.get(locator)
|
|
3719
|
+
if (answering.get(locator) === awaited)
|
|
3569
3720
|
answering.delete(locator);
|
|
3570
3721
|
release();
|
|
3571
3722
|
};
|
|
3572
3723
|
}
|
|
3573
|
-
|
|
3574
|
-
const
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3724
|
+
function declaredAsHeld(room) {
|
|
3725
|
+
const held = replicas.get(room.locator)?.record;
|
|
3726
|
+
return (held === undefined ||
|
|
3727
|
+
(JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
|
|
3728
|
+
JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
|
|
3729
|
+
JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
|
|
3730
|
+
}
|
|
3731
|
+
async function retainModel(local, room, next) {
|
|
3732
|
+
const key = room.locator;
|
|
3733
|
+
const previous = modelOf(room.locator);
|
|
3734
|
+
if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
|
|
3583
3735
|
return;
|
|
3584
3736
|
if (next.status === 'unavailable') {
|
|
3585
3737
|
invalidModels.add(key);
|
|
@@ -3591,26 +3743,13 @@ export function composeRuntime(options) {
|
|
|
3591
3743
|
const held = replicas.get(room.locator);
|
|
3592
3744
|
let changed = null;
|
|
3593
3745
|
if (held !== undefined) {
|
|
3594
|
-
const
|
|
3746
|
+
const record = {
|
|
3595
3747
|
...held.record,
|
|
3596
3748
|
operations: room.operations,
|
|
3597
3749
|
replica: room.replica,
|
|
3598
3750
|
typedSelection: room.typedSelection ?? null,
|
|
3599
|
-
|
|
3751
|
+
model: next,
|
|
3600
3752
|
};
|
|
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
3753
|
const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
|
|
3615
3754
|
const replayedLayer = await replayed(record, confirmedLayer, held.queue);
|
|
3616
3755
|
const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
|
|
@@ -3621,41 +3760,33 @@ export function composeRuntime(options) {
|
|
|
3621
3760
|
key: record.key,
|
|
3622
3761
|
record: storedRecord(record),
|
|
3623
3762
|
roots: record.typedSelection?.roots ?? [],
|
|
3624
|
-
|
|
3763
|
+
composed: composedOf(composed(record)),
|
|
3625
3764
|
overlay: diff.overlay,
|
|
3626
3765
|
},
|
|
3627
3766
|
],
|
|
3628
3767
|
});
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count });
|
|
3768
|
+
const { count, indexed } = committedRoom(room.locator, answer);
|
|
3769
|
+
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
|
|
3632
3770
|
changed = changeSet(room.locator, {
|
|
3633
3771
|
documents: answer.changed[room.locator] ?? [],
|
|
3634
3772
|
room: true,
|
|
3635
3773
|
});
|
|
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
3774
|
}
|
|
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));
|
|
3775
|
+
heldModel(room, next);
|
|
3653
3776
|
if (changed === null)
|
|
3654
3777
|
emitRooms();
|
|
3655
3778
|
else
|
|
3656
3779
|
publishChangeSet(changed);
|
|
3657
3780
|
});
|
|
3658
3781
|
}
|
|
3782
|
+
function heldModel(room, next) {
|
|
3783
|
+
models.set(room.locator, next);
|
|
3784
|
+
if (next.status === 'current') {
|
|
3785
|
+
liveModels.set(room.locator, JSON.stringify(next.snapshot));
|
|
3786
|
+
invalidModels.delete(room.locator);
|
|
3787
|
+
}
|
|
3788
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
3789
|
+
}
|
|
3659
3790
|
async function scoped(result, room, local) {
|
|
3660
3791
|
if (result.scope === 'content' && !result.retryable) {
|
|
3661
3792
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
@@ -3669,14 +3800,14 @@ export function composeRuntime(options) {
|
|
|
3669
3800
|
});
|
|
3670
3801
|
let scopedAway = room;
|
|
3671
3802
|
if (result.scope === 'model')
|
|
3672
|
-
await retainModel(local, room,
|
|
3803
|
+
await retainModel(local, room, {
|
|
3673
3804
|
status: 'unavailable',
|
|
3674
3805
|
reason: 'not-readable',
|
|
3675
3806
|
});
|
|
3676
3807
|
else if (result.scope !== 'operation') {
|
|
3677
3808
|
unreadable.add(room.locator);
|
|
3678
3809
|
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
3679
|
-
await retainModel(local, scopedAway,
|
|
3810
|
+
await retainModel(local, scopedAway, modelOf(room.locator));
|
|
3680
3811
|
}
|
|
3681
3812
|
if (!continuing())
|
|
3682
3813
|
return;
|
|
@@ -3705,10 +3836,12 @@ export function composeRuntime(options) {
|
|
|
3705
3836
|
handed = true;
|
|
3706
3837
|
}
|
|
3707
3838
|
discovering?.release();
|
|
3708
|
-
const
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3839
|
+
for (const room of ready) {
|
|
3840
|
+
if (attempt !== 'unstreamed' && replicas.has(room.locator))
|
|
3841
|
+
continue;
|
|
3842
|
+
void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
|
|
3843
|
+
}
|
|
3844
|
+
const failed = prepared.find((room) => room.status === 'rejected');
|
|
3712
3845
|
if (failed !== undefined)
|
|
3713
3846
|
throw failed.reason;
|
|
3714
3847
|
}
|
|
@@ -3726,7 +3859,7 @@ export function composeRuntime(options) {
|
|
|
3726
3859
|
return false;
|
|
3727
3860
|
}
|
|
3728
3861
|
if (prior !== undefined)
|
|
3729
|
-
await retainModel(local, room,
|
|
3862
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3730
3863
|
if (discoveryPaused || authorization === undefined)
|
|
3731
3864
|
return false;
|
|
3732
3865
|
return room.replica.admission === 'admitted';
|
|
@@ -3739,14 +3872,14 @@ export function composeRuntime(options) {
|
|
|
3739
3872
|
await withhold(withheld, room, local, progress);
|
|
3740
3873
|
}
|
|
3741
3874
|
function revoked(locator) {
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3745
|
-
});
|
|
3875
|
+
const model = modelOf(locator);
|
|
3876
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3746
3877
|
}
|
|
3747
3878
|
async function loadRoom(room, local, progress, live) {
|
|
3748
3879
|
if (!live() || authorization === undefined)
|
|
3749
3880
|
return;
|
|
3881
|
+
if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
|
|
3882
|
+
return;
|
|
3750
3883
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
3751
3884
|
await reload(room, local, progress, live);
|
|
3752
3885
|
return;
|
|
@@ -3764,14 +3897,14 @@ export function composeRuntime(options) {
|
|
|
3764
3897
|
reloadsDue.delete(room.locator);
|
|
3765
3898
|
if (!live() || authorization === undefined)
|
|
3766
3899
|
return;
|
|
3900
|
+
const from = standingOf(room.locator);
|
|
3767
3901
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
3768
|
-
if (!live())
|
|
3769
|
-
return;
|
|
3770
3902
|
if (bootstrapped.kind !== 'staged') {
|
|
3771
|
-
|
|
3903
|
+
if (live())
|
|
3904
|
+
await withhold(bootstrapped, room, local, progress);
|
|
3772
3905
|
return;
|
|
3773
3906
|
}
|
|
3774
|
-
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
3907
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
|
|
3775
3908
|
await caughtUp(room, local, progress, live);
|
|
3776
3909
|
}
|
|
3777
3910
|
finally {
|
|
@@ -3793,6 +3926,8 @@ export function composeRuntime(options) {
|
|
|
3793
3926
|
catch {
|
|
3794
3927
|
result = { kind: 'transport-uncertainty' };
|
|
3795
3928
|
}
|
|
3929
|
+
if (result.kind === 'discovery')
|
|
3930
|
+
roomsNamed = true;
|
|
3796
3931
|
if (!live()) {
|
|
3797
3932
|
progress.complete = false;
|
|
3798
3933
|
notifyReady();
|
|
@@ -3800,12 +3935,7 @@ export function composeRuntime(options) {
|
|
|
3800
3935
|
}
|
|
3801
3936
|
journal.record(`connection discover ${result.kind}`);
|
|
3802
3937
|
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
|
-
}
|
|
3938
|
+
await settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
|
|
3809
3939
|
progress.reachable = false;
|
|
3810
3940
|
progress.complete = false;
|
|
3811
3941
|
notifyReady();
|
|
@@ -3860,6 +3990,7 @@ export function composeRuntime(options) {
|
|
|
3860
3990
|
limits = result.providerLimits;
|
|
3861
3991
|
await local.fact('providerLimits', limits);
|
|
3862
3992
|
await local.fact('capabilities', capabilities);
|
|
3993
|
+
conversations.declared();
|
|
3863
3994
|
}
|
|
3864
3995
|
const signedIn = result.member ?? null;
|
|
3865
3996
|
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
@@ -3882,23 +4013,45 @@ export function composeRuntime(options) {
|
|
|
3882
4013
|
}
|
|
3883
4014
|
}
|
|
3884
4015
|
async function admittedRead(work) {
|
|
3885
|
-
|
|
3886
|
-
|
|
4016
|
+
while (roomReads.free === 0) {
|
|
4017
|
+
await new Promise((admit) => roomReads.waiting.push(admit));
|
|
3887
4018
|
}
|
|
3888
|
-
|
|
3889
|
-
while (reads.free === 0)
|
|
3890
|
-
await new Promise((admit) => reads.waiting.push(admit));
|
|
3891
|
-
reads.free -= 1;
|
|
4019
|
+
roomReads.free -= 1;
|
|
3892
4020
|
try {
|
|
3893
4021
|
await work();
|
|
3894
4022
|
}
|
|
3895
4023
|
finally {
|
|
3896
|
-
|
|
3897
|
-
|
|
4024
|
+
roomReads.free += 1;
|
|
4025
|
+
roomReads.waiting.shift()?.();
|
|
3898
4026
|
}
|
|
3899
4027
|
}
|
|
4028
|
+
function inRoom(locator, work) {
|
|
4029
|
+
roomWork += 1;
|
|
4030
|
+
const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
|
|
4031
|
+
try {
|
|
4032
|
+
return await work(continuing);
|
|
4033
|
+
}
|
|
4034
|
+
finally {
|
|
4035
|
+
roomWork -= 1;
|
|
4036
|
+
}
|
|
4037
|
+
});
|
|
4038
|
+
const chain = ran.then(() => undefined, () => undefined);
|
|
4039
|
+
roomLanes.set(locator, chain);
|
|
4040
|
+
working.add(chain);
|
|
4041
|
+
void chain.finally(() => {
|
|
4042
|
+
working.delete(chain);
|
|
4043
|
+
if (roomLanes.get(locator) === chain)
|
|
4044
|
+
roomLanes.delete(locator);
|
|
4045
|
+
});
|
|
4046
|
+
return ran;
|
|
4047
|
+
}
|
|
3900
4048
|
function roomTask(locator, work) {
|
|
3901
|
-
|
|
4049
|
+
if (stopping)
|
|
4050
|
+
return Promise.resolve();
|
|
4051
|
+
return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
|
|
4052
|
+
}
|
|
4053
|
+
function advanced(room, local, authorized) {
|
|
4054
|
+
return inRoom(room.locator, (live) => advance(room, local, authorized, live));
|
|
3902
4055
|
}
|
|
3903
4056
|
function task(lane, work) {
|
|
3904
4057
|
if (stopping)
|
|
@@ -3917,14 +4070,13 @@ export function composeRuntime(options) {
|
|
|
3917
4070
|
function sync(attempt) {
|
|
3918
4071
|
return task(OPENING_LANE, async (live) => {
|
|
3919
4072
|
if (!live()) {
|
|
3920
|
-
|
|
4073
|
+
readyFromStore();
|
|
3921
4074
|
return;
|
|
3922
4075
|
}
|
|
3923
4076
|
const progress = { complete: true, reachable: true };
|
|
3924
4077
|
const gate = discover();
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
4078
|
+
opened = attempt === 'opened';
|
|
4079
|
+
underway = attempt === 'opened' ? progress : null;
|
|
3928
4080
|
if (attempt === 'opened')
|
|
3929
4081
|
streamed = progress;
|
|
3930
4082
|
complete = false;
|
|
@@ -3958,7 +4110,7 @@ export function composeRuntime(options) {
|
|
|
3958
4110
|
if (handed || closed || progress.reachable)
|
|
3959
4111
|
return;
|
|
3960
4112
|
underway = null;
|
|
3961
|
-
opened =
|
|
4113
|
+
opened = false;
|
|
3962
4114
|
journal.record('connection reopen');
|
|
3963
4115
|
await rest(progress, live);
|
|
3964
4116
|
options.adapter.roomStream?.reopen();
|
|
@@ -3995,13 +4147,12 @@ export function composeRuntime(options) {
|
|
|
3995
4147
|
});
|
|
3996
4148
|
}
|
|
3997
4149
|
function closedSocket(revoked, lost) {
|
|
4150
|
+
notifyDiscovered();
|
|
3998
4151
|
const opened = socketOpen;
|
|
3999
4152
|
socketOpen = false;
|
|
4000
4153
|
connectionLost();
|
|
4001
|
-
if (lost)
|
|
4002
|
-
reads.lost();
|
|
4154
|
+
if (lost)
|
|
4003
4155
|
conversations.lost();
|
|
4004
|
-
}
|
|
4005
4156
|
if (revoked) {
|
|
4006
4157
|
void refuse('revoked-authorization');
|
|
4007
4158
|
return;
|
|
@@ -4016,10 +4167,9 @@ export function composeRuntime(options) {
|
|
|
4016
4167
|
});
|
|
4017
4168
|
}
|
|
4018
4169
|
function applyOpening(expired) {
|
|
4019
|
-
const progress = underway
|
|
4170
|
+
const progress = underway;
|
|
4020
4171
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
4021
|
-
|
|
4022
|
-
opened = null;
|
|
4172
|
+
opened = false;
|
|
4023
4173
|
const reloading = progress === null ? [] : owedReloads(expired);
|
|
4024
4174
|
const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
|
|
4025
4175
|
return task(OPENING_LANE, async (live) => {
|
|
@@ -4031,24 +4181,14 @@ export function composeRuntime(options) {
|
|
|
4031
4181
|
await rest(cycle, live);
|
|
4032
4182
|
return;
|
|
4033
4183
|
}
|
|
4034
|
-
await roomsCarried([...expired, ...reloading]);
|
|
4035
|
-
if (!live())
|
|
4036
|
-
return;
|
|
4037
4184
|
try {
|
|
4038
4185
|
const local = await openLocal();
|
|
4039
|
-
|
|
4186
|
+
await replacing;
|
|
4040
4187
|
if (progress.reachable) {
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
!reloading.includes(room.locator)) {
|
|
4045
|
-
await confirmModels(room, local);
|
|
4046
|
-
}
|
|
4047
|
-
}
|
|
4188
|
+
await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
|
|
4189
|
+
!expired.includes(room.locator) &&
|
|
4190
|
+
!reloading.includes(room.locator)), 'current', local);
|
|
4048
4191
|
}
|
|
4049
|
-
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
4050
|
-
if (failed !== undefined)
|
|
4051
|
-
throw failed.reason;
|
|
4052
4192
|
}
|
|
4053
4193
|
catch {
|
|
4054
4194
|
progress.complete = false;
|
|
@@ -4073,24 +4213,43 @@ export function composeRuntime(options) {
|
|
|
4073
4213
|
async function reloadNow(replaced, progress) {
|
|
4074
4214
|
const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
|
|
4075
4215
|
for (const room of reloaded)
|
|
4076
|
-
reloadsDue.
|
|
4216
|
+
reloadsDue.add(room.locator);
|
|
4077
4217
|
try {
|
|
4078
4218
|
const local = await openLocal();
|
|
4079
|
-
|
|
4219
|
+
for (const room of reloaded) {
|
|
4220
|
+
void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
|
|
4221
|
+
}
|
|
4080
4222
|
}
|
|
4081
4223
|
catch {
|
|
4082
4224
|
for (const room of reloaded)
|
|
4083
4225
|
reloadsDue.delete(room.locator);
|
|
4084
4226
|
progress.reachable = false;
|
|
4085
|
-
return [];
|
|
4086
4227
|
}
|
|
4087
4228
|
}
|
|
4088
|
-
async function
|
|
4089
|
-
|
|
4090
|
-
const model = modelOf(room.locator
|
|
4091
|
-
|
|
4092
|
-
|
|
4229
|
+
async function settleModels(held, status, local) {
|
|
4230
|
+
const owed = (room) => {
|
|
4231
|
+
const model = modelOf(room.locator);
|
|
4232
|
+
return model.status !== 'unavailable' && model.status !== status;
|
|
4233
|
+
};
|
|
4234
|
+
const settled = await exclusive(async () => {
|
|
4235
|
+
if (!continuing())
|
|
4236
|
+
return [];
|
|
4237
|
+
const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
|
|
4238
|
+
for (const room of unchanged) {
|
|
4239
|
+
const next = { ...modelOf(room.locator), status };
|
|
4240
|
+
const replica = replicas.get(room.locator);
|
|
4241
|
+
if (replica !== undefined)
|
|
4242
|
+
replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
|
|
4243
|
+
heldModel(room, next);
|
|
4093
4244
|
}
|
|
4245
|
+
if (unchanged.length > 0)
|
|
4246
|
+
emitRooms();
|
|
4247
|
+
return unchanged;
|
|
4248
|
+
});
|
|
4249
|
+
for (const room of held) {
|
|
4250
|
+
if (settled.includes(room) || !owed(room))
|
|
4251
|
+
continue;
|
|
4252
|
+
await retainModel(local, room, { ...modelOf(room.locator), status });
|
|
4094
4253
|
}
|
|
4095
4254
|
}
|
|
4096
4255
|
function submitLater(locator) {
|
|
@@ -4135,14 +4294,10 @@ export function composeRuntime(options) {
|
|
|
4135
4294
|
try {
|
|
4136
4295
|
const local = await openLocal();
|
|
4137
4296
|
await roomTask(locator, async (live) => {
|
|
4138
|
-
|
|
4139
|
-
if (!current() || authorization === undefined)
|
|
4297
|
+
if (!live() || authorization === undefined)
|
|
4140
4298
|
return;
|
|
4141
|
-
const withheld = await
|
|
4142
|
-
|
|
4143
|
-
abandonedOn(signal),
|
|
4144
|
-
]);
|
|
4145
|
-
if (withheld !== null && current())
|
|
4299
|
+
const withheld = await advance(room, local, authorization, live);
|
|
4300
|
+
if (withheld !== null && live())
|
|
4146
4301
|
await withhold(withheld, room, local, progress);
|
|
4147
4302
|
});
|
|
4148
4303
|
}
|
|
@@ -4197,14 +4352,11 @@ export function composeRuntime(options) {
|
|
|
4197
4352
|
progress.complete = false;
|
|
4198
4353
|
return;
|
|
4199
4354
|
}
|
|
4200
|
-
reads.applied();
|
|
4201
4355
|
submitAll();
|
|
4202
4356
|
for (const locator of stale)
|
|
4203
4357
|
if (repairable.has(locator))
|
|
4204
4358
|
repairLater(locator, {});
|
|
4205
4359
|
await conversations.reconcile();
|
|
4206
|
-
if (status.providerUnreachable)
|
|
4207
|
-
conversations.reconnected();
|
|
4208
4360
|
const reading = { complete: true, reachable: true };
|
|
4209
4361
|
await conversations.catchUp(reading);
|
|
4210
4362
|
if (progress.reachable && reading.reachable)
|
|
@@ -4230,7 +4382,7 @@ export function composeRuntime(options) {
|
|
|
4230
4382
|
if (!live())
|
|
4231
4383
|
return;
|
|
4232
4384
|
let lastCompleted = status.lastCompleted;
|
|
4233
|
-
let synced = complete && stale.size === 0 && !pending();
|
|
4385
|
+
let synced = complete && roomWork === 0 && stale.size === 0 && !pending();
|
|
4234
4386
|
if (synced && !recorded) {
|
|
4235
4387
|
const completedAt = new Date().toISOString();
|
|
4236
4388
|
try {
|
|
@@ -4245,42 +4397,41 @@ export function composeRuntime(options) {
|
|
|
4245
4397
|
}
|
|
4246
4398
|
}
|
|
4247
4399
|
moveTo({
|
|
4248
|
-
state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
|
|
4400
|
+
state: pending() ? 'pending' : synced && complete && roomWork === 0 ? 'synced' : 'syncing',
|
|
4249
4401
|
lastCompleted,
|
|
4250
|
-
providerUnreachable: !progress.reachable,
|
|
4402
|
+
providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
|
|
4251
4403
|
...attention(),
|
|
4252
4404
|
});
|
|
4253
4405
|
}
|
|
4254
4406
|
function discover() {
|
|
4407
|
+
discovering?.release();
|
|
4255
4408
|
let release = () => { };
|
|
4256
4409
|
const done = new Promise((resolve) => {
|
|
4257
4410
|
release = resolve;
|
|
4258
4411
|
});
|
|
4259
|
-
discovering = {
|
|
4412
|
+
discovering = { done, release };
|
|
4260
4413
|
return discovering;
|
|
4261
4414
|
}
|
|
4262
4415
|
function applyRoom(locator, roomVersion) {
|
|
4263
4416
|
announced.set(locator, roomVersion);
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
await saving.answered;
|
|
4417
|
+
if (stopping)
|
|
4418
|
+
return Promise.resolve();
|
|
4419
|
+
return inRoom(locator, async (live) => {
|
|
4420
|
+
await discovering?.done;
|
|
4421
|
+
await answering.get(locator)?.answered;
|
|
4270
4422
|
if (!live() || authorization === undefined)
|
|
4271
|
-
return;
|
|
4423
|
+
return null;
|
|
4272
4424
|
const room = discovered.get(locator);
|
|
4273
4425
|
const held = replicas.get(locator);
|
|
4274
4426
|
if (room === undefined || room.replica.admission !== 'admitted')
|
|
4275
|
-
return;
|
|
4427
|
+
return null;
|
|
4276
4428
|
if (held !== undefined &&
|
|
4277
4429
|
(held.record.replica.selection !== room.replica.selection ||
|
|
4278
4430
|
options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
|
|
4279
|
-
return;
|
|
4431
|
+
return null;
|
|
4280
4432
|
}
|
|
4281
|
-
const
|
|
4282
|
-
|
|
4283
|
-
if (during === null)
|
|
4433
|
+
const progress = underway ?? { complete: true, reachable: true };
|
|
4434
|
+
if (underway === null)
|
|
4284
4435
|
moveTo(transferring(status));
|
|
4285
4436
|
try {
|
|
4286
4437
|
const local = await openLocal();
|
|
@@ -4295,24 +4446,9 @@ export function composeRuntime(options) {
|
|
|
4295
4446
|
catch {
|
|
4296
4447
|
stale.add(locator);
|
|
4297
4448
|
}
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
await rest(progress, live);
|
|
4301
|
-
}
|
|
4302
|
-
});
|
|
4449
|
+
return progress;
|
|
4450
|
+
}).then((progress) => rest(progress), () => undefined);
|
|
4303
4451
|
}
|
|
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
4452
|
function observeStream() {
|
|
4317
4453
|
const stream = options.adapter.roomStream;
|
|
4318
4454
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -4324,7 +4460,6 @@ export function composeRuntime(options) {
|
|
|
4324
4460
|
socketOpen = true;
|
|
4325
4461
|
connectionLive();
|
|
4326
4462
|
generation += 1;
|
|
4327
|
-
reads.opened();
|
|
4328
4463
|
conversations.reopened();
|
|
4329
4464
|
recovery.resume();
|
|
4330
4465
|
void sync('opened');
|
|
@@ -4337,14 +4472,7 @@ export function composeRuntime(options) {
|
|
|
4337
4472
|
void rediscover(event.scope);
|
|
4338
4473
|
else
|
|
4339
4474
|
void applyRoom(event.locator, event.roomVersion);
|
|
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]]));
|
|
4475
|
+
});
|
|
4348
4476
|
}
|
|
4349
4477
|
function writesHeld() {
|
|
4350
4478
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -4414,8 +4542,9 @@ export function composeRuntime(options) {
|
|
|
4414
4542
|
if (closed)
|
|
4415
4543
|
return;
|
|
4416
4544
|
closed = true;
|
|
4417
|
-
|
|
4418
|
-
|
|
4545
|
+
notifyDiscovered();
|
|
4546
|
+
if (probing !== null)
|
|
4547
|
+
clearInterval(probing);
|
|
4419
4548
|
connectionLive();
|
|
4420
4549
|
lockListeners.clear();
|
|
4421
4550
|
readinessWatches.clear();
|
|
@@ -4427,6 +4556,7 @@ export function composeRuntime(options) {
|
|
|
4427
4556
|
conversations.close();
|
|
4428
4557
|
void recovery.retire();
|
|
4429
4558
|
roomListeners.clear();
|
|
4559
|
+
unlistedRooms.clear();
|
|
4430
4560
|
refusalListeners.clear();
|
|
4431
4561
|
storageLossListeners.clear();
|
|
4432
4562
|
statusListeners.clear();
|
|
@@ -4439,6 +4569,7 @@ export function composeRuntime(options) {
|
|
|
4439
4569
|
}
|
|
4440
4570
|
async function stop() {
|
|
4441
4571
|
stopping = true;
|
|
4572
|
+
inFlight.abort();
|
|
4442
4573
|
await recovery.retire();
|
|
4443
4574
|
await Promise.all(working);
|
|
4444
4575
|
await mutations;
|
|
@@ -4452,9 +4583,9 @@ export function composeRuntime(options) {
|
|
|
4452
4583
|
observeStream();
|
|
4453
4584
|
void openLocal().then(() => {
|
|
4454
4585
|
if (capabilities.length > 0)
|
|
4455
|
-
|
|
4586
|
+
readyFromStore();
|
|
4456
4587
|
}, () => {
|
|
4457
|
-
|
|
4588
|
+
readyFromStore();
|
|
4458
4589
|
});
|
|
4459
4590
|
if (options.adapter.roomStream === undefined)
|
|
4460
4591
|
void sync('unstreamed');
|
|
@@ -4602,12 +4733,13 @@ export function composeRuntime(options) {
|
|
|
4602
4733
|
};
|
|
4603
4734
|
}
|
|
4604
4735
|
if (topic === 'readiness') {
|
|
4605
|
-
return watchReadiness({ target: '', code: 'stored',
|
|
4736
|
+
return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
|
|
4606
4737
|
}
|
|
4607
4738
|
if (topic === 'rooms') {
|
|
4608
|
-
|
|
4609
|
-
|
|
4739
|
+
unlistedRooms.add(deliver);
|
|
4740
|
+
emitKnownRooms();
|
|
4610
4741
|
return () => {
|
|
4742
|
+
unlistedRooms.delete(deliver);
|
|
4611
4743
|
roomListeners.delete(deliver);
|
|
4612
4744
|
};
|
|
4613
4745
|
}
|
|
@@ -4661,7 +4793,17 @@ export function composeRuntime(options) {
|
|
|
4661
4793
|
return openLocal().then(() => undefined);
|
|
4662
4794
|
},
|
|
4663
4795
|
rooms() {
|
|
4664
|
-
|
|
4796
|
+
if (closed)
|
|
4797
|
+
return Promise.resolve([]);
|
|
4798
|
+
return new Promise((settle) => {
|
|
4799
|
+
const answer = (value) => {
|
|
4800
|
+
unlistedRooms.delete(answer);
|
|
4801
|
+
roomListeners.delete(answer);
|
|
4802
|
+
settle(value);
|
|
4803
|
+
};
|
|
4804
|
+
unlistedRooms.add(answer);
|
|
4805
|
+
emitKnownRooms();
|
|
4806
|
+
});
|
|
4665
4807
|
},
|
|
4666
4808
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
4667
4809
|
search: (input) => searched(input, null, []),
|
|
@@ -4671,7 +4813,7 @@ export function composeRuntime(options) {
|
|
|
4671
4813
|
typedSelection: rooms.get(locator)?.typedSelection === undefined ||
|
|
4672
4814
|
rooms.get(locator)?.typedSelection === null
|
|
4673
4815
|
? null
|
|
4674
|
-
: modelTypes(
|
|
4816
|
+
: modelTypes(builtIn),
|
|
4675
4817
|
};
|
|
4676
4818
|
},
|
|
4677
4819
|
roomDraft: {
|
|
@@ -4688,33 +4830,17 @@ export function composeRuntime(options) {
|
|
|
4688
4830
|
const record = roomDrafts.get(room)?.get(path);
|
|
4689
4831
|
return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
|
|
4690
4832
|
},
|
|
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
|
-
});
|
|
4833
|
+
write(room, path, revision, input, next = roomDraftRevision()) {
|
|
4834
|
+
if (typeof next !== 'string' || next === '')
|
|
4835
|
+
return Promise.reject(runtimeError('room-draft-malformed'));
|
|
4836
|
+
return gathered(`room-draft\u0000${room}\u0000${path}`, { revision, input, next }, (earlier, later) => {
|
|
4837
|
+
if (later.revision !== earlier.next)
|
|
4838
|
+
return null;
|
|
4839
|
+
const parts = gatheredParts(earlier.input, later.input);
|
|
4840
|
+
return parts === null
|
|
4841
|
+
? null
|
|
4842
|
+
: { revision: earlier.revision, input: parts, next: later.next };
|
|
4843
|
+
}, (written) => writtenRoomDraft(room, path, written.revision, written.input, written.next));
|
|
4718
4844
|
},
|
|
4719
4845
|
discard(room, path, revision) {
|
|
4720
4846
|
return exclusive(async () => {
|
|
@@ -4741,13 +4867,13 @@ export function composeRuntime(options) {
|
|
|
4741
4867
|
});
|
|
4742
4868
|
},
|
|
4743
4869
|
write(app, value) {
|
|
4744
|
-
return
|
|
4870
|
+
return exclusive(() => keptPresentation(app, () => structuredClone(value)));
|
|
4745
4871
|
},
|
|
4746
4872
|
edit(app, members) {
|
|
4747
|
-
return
|
|
4748
|
-
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(
|
|
4749
|
-
...Object.entries(
|
|
4750
|
-
]));
|
|
4873
|
+
return gathered(`presentation\u0000${app}`, structuredClone(members), (earlier, later) => ({ ...earlier, ...later }), (edited) => keptPresentation(app, (held) => Object.fromEntries([
|
|
4874
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(edited, member)),
|
|
4875
|
+
...Object.entries(edited).filter(([, value]) => value !== null),
|
|
4876
|
+
])));
|
|
4751
4877
|
},
|
|
4752
4878
|
},
|
|
4753
4879
|
save(room, named, writtenBy) {
|
|
@@ -4766,7 +4892,8 @@ export function composeRuntime(options) {
|
|
|
4766
4892
|
part: readPart,
|
|
4767
4893
|
read: readBinaryPath,
|
|
4768
4894
|
link: linkBinaryPath,
|
|
4769
|
-
|
|
4895
|
+
show: showBinaryPath,
|
|
4896
|
+
attach: attachBinary,
|
|
4770
4897
|
},
|
|
4771
4898
|
async call(name, input) {
|
|
4772
4899
|
await ready;
|
|
@@ -4789,7 +4916,8 @@ export function composeRuntime(options) {
|
|
|
4789
4916
|
journal.record('retry');
|
|
4790
4917
|
unpreparable.clear();
|
|
4791
4918
|
queueReadiness();
|
|
4792
|
-
|
|
4919
|
+
if (full)
|
|
4920
|
+
await probed();
|
|
4793
4921
|
},
|
|
4794
4922
|
observe: observing(watch),
|
|
4795
4923
|
async reauthorize() {
|
|
@@ -4811,7 +4939,6 @@ export function composeRuntime(options) {
|
|
|
4811
4939
|
const reachable = {
|
|
4812
4940
|
md,
|
|
4813
4941
|
watch,
|
|
4814
|
-
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
4815
4942
|
plan: planned,
|
|
4816
4943
|
query: (plan) => rowsQuery(plan),
|
|
4817
4944
|
watchPlan,
|
|
@@ -4821,6 +4948,7 @@ export function composeRuntime(options) {
|
|
|
4821
4948
|
lockedRooms,
|
|
4822
4949
|
watchLocks,
|
|
4823
4950
|
located,
|
|
4951
|
+
room: (locator) => listed().find((one) => one.locator === locator),
|
|
4824
4952
|
};
|
|
4825
4953
|
return {
|
|
4826
4954
|
md,
|
|
@@ -4835,6 +4963,10 @@ export function composeRuntime(options) {
|
|
|
4835
4963
|
journal.record(`persistence ${answer}`);
|
|
4836
4964
|
queueReadiness();
|
|
4837
4965
|
},
|
|
4838
|
-
within: (reach, port) =>
|
|
4966
|
+
within: (reach, port) => {
|
|
4967
|
+
if (full)
|
|
4968
|
+
void probed();
|
|
4969
|
+
return narrowed(reachable, reach, port);
|
|
4970
|
+
},
|
|
4839
4971
|
};
|
|
4840
4972
|
}
|