@agentshouse/mdruntime 0.1.0-cf890bb538119 → 0.1.0-cf9a84afc6def
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 +59 -59
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attached-model.d.ts +2 -1
- package/dist/attached-model.d.ts.map +1 -1
- package/dist/attached-model.js +28 -4
- package/dist/binary.d.ts +0 -5
- package/dist/binary.d.ts.map +1 -1
- package/dist/binary.js +1 -1
- package/dist/bootstrap.d.ts +16 -8
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +142 -94
- package/dist/client.d.ts +8 -10
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +54 -8
- package/dist/collection.d.ts +5 -3
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +24 -19
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +95 -30
- package/dist/connection.d.ts +8 -8
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1242 -1064
- package/dist/conversation.d.ts +12 -11
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +201 -131
- package/dist/durability.d.ts +1 -5
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +5 -0
- package/dist/entry.d.ts +1 -0
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +1 -1
- package/dist/file.d.ts +2 -11
- package/dist/file.d.ts.map +1 -1
- package/dist/file.js +6 -4
- package/dist/fulltext.d.ts.map +1 -1
- package/dist/fulltext.js +8 -6
- package/dist/host.d.ts +3 -4
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +40 -20
- package/dist/index.d.ts +10 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/media.d.ts +8 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +218 -0
- package/dist/memory.d.ts +8 -6
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +165 -96
- 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 +66 -50
- 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 +9 -4
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +64 -30
- package/dist/replica.d.ts +5 -5
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +4 -7
- package/dist/room-draft.d.ts +18 -2
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +73 -6
- package/dist/rows.d.ts +15 -6
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +102 -74
- package/dist/save.d.ts +2 -0
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +13 -7
- package/dist/shared.d.ts +0 -1
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +0 -10
- package/dist/status.d.ts +4 -3
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +3 -1
- package/dist/store.d.ts +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 -9
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +2 -3
- package/dist/words.d.ts.map +1 -1
- package/dist/words.js +8 -2
- package/package.json +11 -18
- package/AGENTS.md +0 -319
- package/dist/chat/acts.d.ts +0 -33
- package/dist/chat/acts.d.ts.map +0 -1
- package/dist/chat/acts.js +0 -36
- package/dist/chat/context.d.ts +0 -3
- package/dist/chat/context.d.ts.map +0 -1
- package/dist/chat/context.js +0 -10
- package/dist/chat/earlier.d.ts +0 -10
- package/dist/chat/earlier.d.ts.map +0 -1
- package/dist/chat/earlier.js +0 -54
- package/dist/chat/house.d.ts +0 -25
- package/dist/chat/house.d.ts.map +0 -1
- package/dist/chat/house.js +0 -38
- package/dist/chat/input.d.ts +0 -11
- package/dist/chat/input.d.ts.map +0 -1
- package/dist/chat/input.js +0 -92
- package/dist/chat/kit.d.ts +0 -61
- package/dist/chat/kit.d.ts.map +0 -1
- package/dist/chat/kit.js +0 -123
- package/dist/chat/lasting.d.ts +0 -6
- package/dist/chat/lasting.d.ts.map +0 -1
- package/dist/chat/lasting.js +0 -19
- package/dist/chat/list.d.ts +0 -28
- package/dist/chat/list.d.ts.map +0 -1
- package/dist/chat/list.js +0 -103
- package/dist/chat/markdown.d.ts +0 -21
- package/dist/chat/markdown.d.ts.map +0 -1
- package/dist/chat/markdown.js +0 -246
- package/dist/chat/message.d.ts +0 -7
- package/dist/chat/message.d.ts.map +0 -1
- package/dist/chat/message.js +0 -254
- package/dist/chat/messages.d.ts +0 -35
- package/dist/chat/messages.d.ts.map +0 -1
- package/dist/chat/messages.js +0 -210
- package/dist/chat/name.d.ts +0 -7
- package/dist/chat/name.d.ts.map +0 -1
- package/dist/chat/name.js +0 -37
- package/dist/chat/opened.d.ts +0 -12
- package/dist/chat/opened.d.ts.map +0 -1
- package/dist/chat/opened.js +0 -19
- package/dist/chat/overlay.d.ts +0 -32
- package/dist/chat/overlay.d.ts.map +0 -1
- package/dist/chat/overlay.js +0 -103
- package/dist/chat/parts.d.ts +0 -58
- package/dist/chat/parts.d.ts.map +0 -1
- package/dist/chat/parts.js +0 -120
- package/dist/chat/pieces.d.ts +0 -62
- package/dist/chat/pieces.d.ts.map +0 -1
- package/dist/chat/pieces.js +0 -1110
- package/dist/chat/process.d.ts +0 -20
- package/dist/chat/process.d.ts.map +0 -1
- package/dist/chat/process.js +0 -32
- package/dist/chat/provider.d.ts +0 -70
- package/dist/chat/provider.d.ts.map +0 -1
- package/dist/chat/provider.js +0 -635
- package/dist/chat/question.d.ts +0 -9
- package/dist/chat/question.d.ts.map +0 -1
- package/dist/chat/question.js +0 -69
- package/dist/chat/questions.d.ts +0 -31
- package/dist/chat/questions.d.ts.map +0 -1
- package/dist/chat/questions.js +0 -111
- package/dist/chat/read.d.ts +0 -5
- package/dist/chat/read.d.ts.map +0 -1
- package/dist/chat/read.js +0 -13
- package/dist/chat/ready.d.ts +0 -37
- package/dist/chat/ready.d.ts.map +0 -1
- package/dist/chat/ready.js +0 -93
- package/dist/chat/room-file.d.ts +0 -8
- package/dist/chat/room-file.d.ts.map +0 -1
- package/dist/chat/room-file.js +0 -72
- package/dist/chat/steady.d.ts +0 -3
- package/dist/chat/steady.d.ts.map +0 -1
- package/dist/chat/steady.js +0 -26
- package/dist/chat/style.d.ts +0 -4
- package/dist/chat/style.d.ts.map +0 -1
- package/dist/chat/style.js +0 -307
- package/dist/chat.d.ts +0 -15
- package/dist/chat.d.ts.map +0 -1
- package/dist/chat.js +0 -8
- package/dist/reads.d.ts +0 -11
- package/dist/reads.d.ts.map +0 -1
- package/dist/reads.js +0 -98
package/dist/connection.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { attachedModel,
|
|
1
|
+
import { parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
|
|
2
|
+
import { attachedModel, builtInModel, roomTypesOf } from './attached-model.js';
|
|
3
3
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
4
4
|
import { observing, whole } from './observation.js';
|
|
5
5
|
import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
|
|
@@ -12,29 +12,28 @@ import { bootstrapRoom } from './bootstrap.js';
|
|
|
12
12
|
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
13
13
|
import { createConversationStore, } from './conversation.js';
|
|
14
14
|
import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
|
|
15
|
-
import { available, collectionOf,
|
|
15
|
+
import { available, collectionOf, composedOf, validRoot, } from './collection.js';
|
|
16
16
|
import { runtimeError } from './failure.js';
|
|
17
17
|
import { createLog } from './log.js';
|
|
18
18
|
import { binaryRecord } from './binary.js';
|
|
19
|
+
import { attachedFile } from './file.js';
|
|
19
20
|
import { openedFiles } from './opened.js';
|
|
20
|
-
import { ABSENT_TARGET, absentBinary,
|
|
21
|
+
import { ABSENT_TARGET, absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
|
|
21
22
|
import { createRecovery } from './recovery.js';
|
|
22
|
-
import { doorReads } from './reads.js';
|
|
23
23
|
import { advancement } from './refresh.js';
|
|
24
24
|
import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
|
|
25
|
-
import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths,
|
|
26
|
-
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
25
|
+
import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
|
|
26
|
+
import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, readSavedParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
27
27
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
28
|
-
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
29
|
-
import { readinessOf,
|
|
28
|
+
import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
|
|
29
|
+
import { readinessOf, } from './readiness.js';
|
|
30
30
|
import { FencedMap } from './fence.js';
|
|
31
|
-
import { fingerprint } from './stored.js';
|
|
32
31
|
import { sameBinary } from './overlay.js';
|
|
33
32
|
import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
34
33
|
import { identityTime, roomDraftRevision } from './identity.js';
|
|
35
|
-
import { beneath, normalize } from './paths.js';
|
|
34
|
+
import { ancestors, beneath, normalize } from './paths.js';
|
|
36
35
|
import { openRuntimeStore, } from './store.js';
|
|
37
|
-
import {
|
|
36
|
+
import { validatedNeighbourhood } from './validation.js';
|
|
38
37
|
import { unconfirmed } from './transfer.js';
|
|
39
38
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
40
39
|
function within(root, path) {
|
|
@@ -55,34 +54,43 @@ const CONVERSATION_LANE = 'conversations';
|
|
|
55
54
|
const ROOM_READS = 256;
|
|
56
55
|
const ROOM_ATTEMPTS = 16;
|
|
57
56
|
const ERASE_BATCH = 512;
|
|
58
|
-
const
|
|
59
|
-
const
|
|
57
|
+
const DERIVE_BYTES = 1 << 23;
|
|
58
|
+
const DERIVE_FLOOR = 1 << 18;
|
|
59
|
+
// An index build aims at this much database work, so a Save, a query or the next part's progress waits behind at most one short build.
|
|
60
|
+
const DERIVE_AIM_MS = 250;
|
|
61
|
+
// A build past its floor of text stops after a second and a half of work, its full-text writes included, so a Room whose text costs ten times the usual holds a Save, a query or the next part's progress behind no longer build.
|
|
62
|
+
const DERIVE_LONGEST_MS = 1_500;
|
|
60
63
|
const QUERY_BYTES = 1 << 20;
|
|
61
64
|
const STAGE_BYTES = 1 << 20;
|
|
62
|
-
const
|
|
65
|
+
const STAGE_FLOOR = 1 << 16;
|
|
66
|
+
// A batch aims at this much database work, so a Save or a query waits behind at most one short write.
|
|
67
|
+
const STAGE_AIM_MS = 50;
|
|
68
|
+
const STREAMS_HELD = 4;
|
|
69
|
+
const NOTHING_HELD = { bytes: 0 };
|
|
70
|
+
const QUEUED_TEXT = 1 << 23;
|
|
71
|
+
// Bytes still arriving are reported at most this often, so readiness is not rebuilt for every chunk the network hands over while a slow link still moves the figure.
|
|
72
|
+
const ARRIVAL_REPORT_MS = 250;
|
|
63
73
|
// A Room that locks offline stays readable this long after the connection drops, so a laptop waking or changing network keeps working while a disconnected device locks soon after.
|
|
64
74
|
const LOCK_GRACE_MS = 120_000;
|
|
65
75
|
function operationOf(input) {
|
|
66
76
|
const operation = input?.operation_id;
|
|
67
77
|
return typeof operation === 'string' ? operation : null;
|
|
68
78
|
}
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
function readUntil(read, signal) {
|
|
80
|
+
if (signal.aborted)
|
|
81
|
+
return Promise.resolve({ kind: 'transport-uncertainty' });
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const stop = () => resolve({ kind: 'transport-uncertainty' });
|
|
84
|
+
signal.addEventListener('abort', stop, { once: true });
|
|
85
|
+
read.then(resolve, reject).finally(() => signal.removeEventListener('abort', stop));
|
|
75
86
|
});
|
|
76
87
|
}
|
|
77
|
-
function roomLane(locator) {
|
|
78
|
-
return `room ${locator}`;
|
|
79
|
-
}
|
|
80
88
|
function pendingKey(locator, id) {
|
|
81
89
|
return `${locator}\u0000${id}`;
|
|
82
90
|
}
|
|
83
91
|
export function composeRuntime(options) {
|
|
84
92
|
const localSaves = options.adapter.localSaves === true;
|
|
85
|
-
const roomTypes = roomTypesOf(options.adapter.roomTypes);
|
|
93
|
+
const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
|
|
86
94
|
let authorization = options.authorization;
|
|
87
95
|
let opening;
|
|
88
96
|
const fences = new Map();
|
|
@@ -92,44 +100,51 @@ export function composeRuntime(options) {
|
|
|
92
100
|
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
93
101
|
let discoveries = 0;
|
|
94
102
|
const replicas = new FencedMap(isFenced);
|
|
103
|
+
const reopened = new Set();
|
|
104
|
+
const usableWhenHeld = options.usableWhenHeld ?? [];
|
|
95
105
|
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
96
106
|
const pendings = new Map();
|
|
97
107
|
const seeds = new Map();
|
|
98
108
|
const seedWatchers = new Map();
|
|
99
109
|
const roomDrafts = new FencedMap(isFenced);
|
|
110
|
+
const attachedFiles = new FencedMap(isFenced);
|
|
100
111
|
const presentations = new Map();
|
|
101
112
|
let limits = [];
|
|
102
113
|
let capabilities = [];
|
|
103
114
|
const rooms = new FencedMap(isFenced);
|
|
104
115
|
const counted = new Map();
|
|
105
116
|
const unreadable = new Set();
|
|
106
|
-
const
|
|
107
|
-
const models = new FencedMap(
|
|
108
|
-
const liveModels = new FencedMap(
|
|
117
|
+
const builtIn = builtInModel(roomTypes);
|
|
118
|
+
const models = new FencedMap(isFenced);
|
|
119
|
+
const liveModels = new FencedMap(isFenced);
|
|
109
120
|
const invalidModels = new Set();
|
|
110
121
|
const discovered = new Map();
|
|
111
|
-
|
|
122
|
+
let inFlight = new AbortController();
|
|
123
|
+
const refreshed = options.adapter.refresh?.bind(options.adapter);
|
|
124
|
+
const reads = {
|
|
125
|
+
bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
|
|
126
|
+
...(refreshed === undefined
|
|
127
|
+
? {}
|
|
128
|
+
: {
|
|
129
|
+
refresh: (authorized, request) => readUntil(refreshed(authorized, request), inFlight.signal),
|
|
130
|
+
}),
|
|
131
|
+
reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
|
|
132
|
+
};
|
|
112
133
|
let roomsListed = false;
|
|
113
134
|
const scopedRefusals = new Map();
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
function modelKey(locator, attached) {
|
|
117
|
-
return `${attached} ${locator}`;
|
|
118
|
-
}
|
|
119
|
-
function modelOf(locator, attached = 'collection') {
|
|
120
|
-
return models.get(modelKey(locator, attached)) ?? NO_MODEL;
|
|
135
|
+
function modelOf(locator) {
|
|
136
|
+
return models.get(locator) ?? NO_MODEL;
|
|
121
137
|
}
|
|
122
|
-
function savedModel(locator
|
|
123
|
-
return invalidModels.has(
|
|
138
|
+
function savedModel(locator) {
|
|
139
|
+
return invalidModels.has(locator)
|
|
124
140
|
? { status: 'unavailable', reason: 'revoked' }
|
|
125
|
-
: modelOf(locator
|
|
141
|
+
: modelOf(locator);
|
|
142
|
+
}
|
|
143
|
+
function composed(record) {
|
|
144
|
+
return compositionOf(record, builtIn);
|
|
126
145
|
}
|
|
127
146
|
function compositionFor(record) {
|
|
128
|
-
return
|
|
129
|
-
...record,
|
|
130
|
-
model: savedModel(record.locator, 'collection'),
|
|
131
|
-
selectionModel: savedModel(record.locator, 'typed-selection'),
|
|
132
|
-
});
|
|
147
|
+
return composed({ ...record, model: savedModel(record.locator) });
|
|
133
148
|
}
|
|
134
149
|
let refusal = null;
|
|
135
150
|
let storageLoss = null;
|
|
@@ -139,10 +154,10 @@ export function composeRuntime(options) {
|
|
|
139
154
|
let closed = false;
|
|
140
155
|
let stopping = false;
|
|
141
156
|
let withdrawStream;
|
|
142
|
-
const announced = new Map();
|
|
143
157
|
let underway = null;
|
|
144
158
|
let streamed = null;
|
|
145
159
|
let handed = false;
|
|
160
|
+
let naming = 0;
|
|
146
161
|
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
147
162
|
const repairing = new Set();
|
|
148
163
|
const calling = new Set();
|
|
@@ -153,15 +168,16 @@ export function composeRuntime(options) {
|
|
|
153
168
|
let recorded = false;
|
|
154
169
|
const stale = new Set();
|
|
155
170
|
let generation = 0;
|
|
156
|
-
|
|
157
|
-
let opened = null;
|
|
158
|
-
const owed = new Set();
|
|
159
|
-
const reloadsDue = new Map();
|
|
171
|
+
const reloadsDue = new Set();
|
|
160
172
|
const lanes = new Map();
|
|
161
|
-
|
|
173
|
+
const roomLanes = new Map();
|
|
174
|
+
let roomWork = 0;
|
|
175
|
+
const roomReads = { free: ROOM_READS, waiting: [] };
|
|
162
176
|
const working = new Set();
|
|
163
177
|
let mutations = Promise.resolve();
|
|
178
|
+
let gathering = null;
|
|
164
179
|
const roomListeners = new Set();
|
|
180
|
+
const unlistedRooms = new Set();
|
|
165
181
|
const refusalListeners = new Set();
|
|
166
182
|
const storageLossListeners = new Set();
|
|
167
183
|
const statusListeners = new Set();
|
|
@@ -172,6 +188,7 @@ export function composeRuntime(options) {
|
|
|
172
188
|
let withdrawConversations;
|
|
173
189
|
let currencyQueued = null;
|
|
174
190
|
let socketOpen = false;
|
|
191
|
+
let announced = false;
|
|
175
192
|
const confirmed = new Set();
|
|
176
193
|
let lockGrace = null;
|
|
177
194
|
const lockListeners = new Set();
|
|
@@ -181,49 +198,80 @@ export function composeRuntime(options) {
|
|
|
181
198
|
const serviceModeListeners = new Set();
|
|
182
199
|
let member = null;
|
|
183
200
|
const memberListeners = new Set();
|
|
184
|
-
const prepared = new Set();
|
|
185
201
|
const unpreparedDemand = new Set();
|
|
186
202
|
const unpreparable = new Set();
|
|
187
203
|
let preparing = false;
|
|
188
204
|
let targets = {};
|
|
189
205
|
let persistence = options.shell.persistence;
|
|
190
|
-
let
|
|
191
|
-
|
|
192
|
-
let
|
|
206
|
+
let full = false;
|
|
207
|
+
let probing = null;
|
|
208
|
+
let testing = null;
|
|
209
|
+
const database = {
|
|
210
|
+
async ask(request) {
|
|
211
|
+
try {
|
|
212
|
+
return await options.database.ask(request);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
if (refusedForSpace(error))
|
|
216
|
+
filled();
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
};
|
|
193
221
|
const readinessWatches = new Set();
|
|
194
222
|
let readinessQueued = false;
|
|
195
223
|
const catching = new Set();
|
|
196
224
|
const transfers = new FencedMap(isFenced);
|
|
225
|
+
const installs = new Set();
|
|
226
|
+
const arrivals = new Map();
|
|
227
|
+
const streaming = new Set();
|
|
228
|
+
const underived = new Map();
|
|
229
|
+
const unindexed = new Map();
|
|
230
|
+
const startedOver = new Map();
|
|
231
|
+
let arrivalReported = 0;
|
|
232
|
+
let streamsFree = STREAMS_HELD;
|
|
233
|
+
let queuedText = 0;
|
|
234
|
+
const installWaiters = [];
|
|
235
|
+
const stageBudget = {
|
|
236
|
+
bytes: STAGE_FLOOR * 4,
|
|
237
|
+
floor: STAGE_FLOOR,
|
|
238
|
+
ceiling: STAGE_BYTES,
|
|
239
|
+
aim: STAGE_AIM_MS,
|
|
240
|
+
};
|
|
241
|
+
const deriveBudget = {
|
|
242
|
+
bytes: DERIVE_FLOOR * 4,
|
|
243
|
+
floor: DERIVE_FLOOR,
|
|
244
|
+
ceiling: DERIVE_BYTES,
|
|
245
|
+
aim: DERIVE_AIM_MS,
|
|
246
|
+
};
|
|
247
|
+
const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
|
|
197
248
|
const conversations = createConversationStore({
|
|
198
|
-
agents: declaredAgents,
|
|
199
|
-
documents: (declaration, after) => rowsQuery(planOf({
|
|
200
|
-
rooms: [declaration.locator],
|
|
201
|
-
types: [declaration.type],
|
|
202
|
-
select: {
|
|
203
|
-
fields: [
|
|
204
|
-
declaration.identity,
|
|
205
|
-
declaration.name,
|
|
206
|
-
...(declaration.retired === undefined ? [] : [declaration.retired]),
|
|
207
|
-
].map((field) => `/${escapePointer(field)}`),
|
|
208
|
-
},
|
|
209
|
-
limit: MAXIMUM_WINDOW,
|
|
210
|
-
...(after === undefined ? {} : { after }),
|
|
211
|
-
}, roomFacts())),
|
|
212
249
|
call: (name, input) => invokeAction(name, input),
|
|
250
|
+
history: (request) => readConversationHistory(request),
|
|
213
251
|
durably,
|
|
214
252
|
stream: () => options.adapter.conversations,
|
|
215
253
|
publish: publishChangeSet,
|
|
216
254
|
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
255
|
+
full: () => full,
|
|
217
256
|
closed: () => closed,
|
|
218
257
|
unanswered: resendLater,
|
|
219
258
|
expired: (operation) => pastHorizon(operation),
|
|
220
259
|
});
|
|
221
260
|
const journal = createLog();
|
|
222
|
-
const keptFiles = openedFiles(
|
|
223
|
-
let
|
|
261
|
+
const keptFiles = openedFiles(database, isFenced);
|
|
262
|
+
let readyFromStore = () => { };
|
|
224
263
|
const ready = new Promise((resolve) => {
|
|
225
|
-
|
|
264
|
+
readyFromStore = resolve;
|
|
265
|
+
});
|
|
266
|
+
let notifyDiscovered = () => { };
|
|
267
|
+
const firstDiscovery = new Promise((resolve) => {
|
|
268
|
+
notifyDiscovered = resolve;
|
|
226
269
|
});
|
|
270
|
+
let roomsNamed = false;
|
|
271
|
+
function notifyReady() {
|
|
272
|
+
notifyDiscovered();
|
|
273
|
+
readyFromStore();
|
|
274
|
+
}
|
|
227
275
|
function rememberRoomDraft(record) {
|
|
228
276
|
const room = roomDrafts.get(record.locator) ?? new Map();
|
|
229
277
|
room.set(record.path, record);
|
|
@@ -243,7 +291,7 @@ export function composeRuntime(options) {
|
|
|
243
291
|
return undefined;
|
|
244
292
|
const local = await openLocal();
|
|
245
293
|
const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
|
|
246
|
-
return { ...slice, collection: collectionOf(slice.documents,
|
|
294
|
+
return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
|
|
247
295
|
}
|
|
248
296
|
function roomDraftOf(record, visible) {
|
|
249
297
|
return structuredClone({
|
|
@@ -299,14 +347,40 @@ export function composeRuntime(options) {
|
|
|
299
347
|
const value = presentations.get(app);
|
|
300
348
|
return value === undefined ? null : structuredClone(value);
|
|
301
349
|
}
|
|
302
|
-
function
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
350
|
+
async function writtenRoomDraft(room, path, revision, input, next) {
|
|
351
|
+
const parts = readParts(input);
|
|
352
|
+
if (path === '' || normalize(path) !== path)
|
|
353
|
+
throw runtimeError('room-draft-malformed');
|
|
354
|
+
const local = await openLocal();
|
|
355
|
+
const current = roomDrafts.get(room)?.get(path);
|
|
356
|
+
if ((current?.revision ?? null) !== revision) {
|
|
357
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
358
|
+
}
|
|
359
|
+
const file = parts.file === undefined
|
|
360
|
+
? undefined
|
|
361
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined
|
|
362
|
+
? binaryBase(await visibleOf(room, [path]), path)
|
|
363
|
+
: parts.file.base);
|
|
364
|
+
const record = {
|
|
365
|
+
locator: room,
|
|
366
|
+
path,
|
|
367
|
+
revision: next,
|
|
368
|
+
parts: held(current?.parts, parts, file),
|
|
369
|
+
returned: current?.returned ?? null,
|
|
370
|
+
};
|
|
371
|
+
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
372
|
+
? []
|
|
373
|
+
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
374
|
+
rememberRoomDraft(record);
|
|
375
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
376
|
+
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
377
|
+
}
|
|
378
|
+
async function keptPresentation(app, next) {
|
|
379
|
+
const local = await openLocal();
|
|
380
|
+
const stored = next(presentations.get(app));
|
|
381
|
+
await local.present({ app, value: stored });
|
|
382
|
+
presentations.set(app, stored);
|
|
383
|
+
publishChangeSet(changeSet('', { presentation: app }));
|
|
310
384
|
}
|
|
311
385
|
function emitPresentation(app) {
|
|
312
386
|
if (closed)
|
|
@@ -321,6 +395,20 @@ export function composeRuntime(options) {
|
|
|
321
395
|
(queuesWithoutReplica.size > 0 ||
|
|
322
396
|
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
323
397
|
}
|
|
398
|
+
function unsent() {
|
|
399
|
+
const changed = new Map();
|
|
400
|
+
for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
401
|
+
for (const { batch, state } of entriesOf(room)) {
|
|
402
|
+
if (state === 'local')
|
|
403
|
+
continue;
|
|
404
|
+
for (const { path } of batch.changes)
|
|
405
|
+
changed.set(`${room}\u0000${path}`, { room, path });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return [...changed]
|
|
409
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
410
|
+
.map(([, change]) => change);
|
|
411
|
+
}
|
|
324
412
|
function attention() {
|
|
325
413
|
const attentionRequired = [];
|
|
326
414
|
for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
|
|
@@ -332,16 +420,9 @@ export function composeRuntime(options) {
|
|
|
332
420
|
returned: structuredClone(record.returned),
|
|
333
421
|
});
|
|
334
422
|
}
|
|
335
|
-
for (const [room, refusal] of sourceRefusals)
|
|
336
|
-
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
337
423
|
attentionRequired.sort((left, right) => {
|
|
338
424
|
if (left.room !== right.room)
|
|
339
425
|
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
426
|
if (left.path !== right.path)
|
|
346
427
|
return left.path < right.path ? -1 : 1;
|
|
347
428
|
return 0;
|
|
@@ -353,7 +434,7 @@ export function composeRuntime(options) {
|
|
|
353
434
|
if (reset === null || submission.reset < reset)
|
|
354
435
|
reset = submission.reset;
|
|
355
436
|
}
|
|
356
|
-
return { attentionRequired, reset };
|
|
437
|
+
return { attentionRequired, unsent: unsent(), reset };
|
|
357
438
|
}
|
|
358
439
|
function documentsIn(locator) {
|
|
359
440
|
return replicas.get(locator)?.count ?? 0;
|
|
@@ -426,7 +507,7 @@ export function composeRuntime(options) {
|
|
|
426
507
|
const governed = governs(reach, room);
|
|
427
508
|
const attached = [
|
|
428
509
|
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
429
|
-
...(governed.selection ? [
|
|
510
|
+
...(governed.selection ? [builtIn] : []),
|
|
430
511
|
];
|
|
431
512
|
return {
|
|
432
513
|
locator: room.locator,
|
|
@@ -442,11 +523,26 @@ export function composeRuntime(options) {
|
|
|
442
523
|
const value = listed();
|
|
443
524
|
for (const listener of roomListeners)
|
|
444
525
|
listener(value);
|
|
526
|
+
emitKnownRooms();
|
|
527
|
+
}
|
|
528
|
+
function emitKnownRooms() {
|
|
529
|
+
conversations.learned();
|
|
530
|
+
if (unlistedRooms.size === 0)
|
|
531
|
+
return;
|
|
532
|
+
if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
|
|
533
|
+
return;
|
|
534
|
+
const value = listed();
|
|
535
|
+
for (const listener of [...unlistedRooms]) {
|
|
536
|
+
unlistedRooms.delete(listener);
|
|
537
|
+
roomListeners.add(listener);
|
|
538
|
+
listener(value);
|
|
539
|
+
}
|
|
445
540
|
}
|
|
446
541
|
function emitRefusal() {
|
|
447
542
|
queueReadiness();
|
|
448
543
|
for (const listener of refusalListeners)
|
|
449
544
|
listener(refusal);
|
|
545
|
+
emitKnownRooms();
|
|
450
546
|
}
|
|
451
547
|
function emitStorageLoss() {
|
|
452
548
|
queueReadiness();
|
|
@@ -467,6 +563,7 @@ export function composeRuntime(options) {
|
|
|
467
563
|
if (closed)
|
|
468
564
|
return;
|
|
469
565
|
queueReadiness();
|
|
566
|
+
emitKnownRooms();
|
|
470
567
|
const next = status.state === 'synced' && !conversations.current()
|
|
471
568
|
? { ...status, state: 'syncing' }
|
|
472
569
|
: status;
|
|
@@ -480,9 +577,6 @@ export function composeRuntime(options) {
|
|
|
480
577
|
if (localSaves)
|
|
481
578
|
return Object.freeze({ batch, state: 'local' });
|
|
482
579
|
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
580
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
487
581
|
}
|
|
488
582
|
function entriesOf(locator) {
|
|
@@ -501,18 +595,25 @@ export function composeRuntime(options) {
|
|
|
501
595
|
function unpublished(error) {
|
|
502
596
|
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
503
597
|
}
|
|
598
|
+
function indexed(locator) {
|
|
599
|
+
return replicas.get(locator)?.indexed === true;
|
|
600
|
+
}
|
|
504
601
|
async function prepareReplica(locator) {
|
|
505
602
|
for (;;) {
|
|
506
603
|
const held = replicas.get(locator);
|
|
507
|
-
if (held === undefined || closed ||
|
|
604
|
+
if (held === undefined || closed || held.indexed)
|
|
508
605
|
return;
|
|
509
606
|
const key = held.record.key;
|
|
510
|
-
const done = await exclusive(async () =>
|
|
511
|
-
|
|
607
|
+
const done = await exclusive(async () => {
|
|
608
|
+
const local = await openLocal();
|
|
609
|
+
return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
|
|
610
|
+
});
|
|
611
|
+
const now = replicas.get(locator);
|
|
612
|
+
if (now?.record.key !== key)
|
|
512
613
|
continue;
|
|
513
|
-
if (!done)
|
|
614
|
+
if (!done.done)
|
|
514
615
|
continue;
|
|
515
|
-
|
|
616
|
+
replicas.set(locator, { ...now, indexed: true });
|
|
516
617
|
publishChangeSet(changeSet(locator, { documents: null }));
|
|
517
618
|
liveQueries.recover();
|
|
518
619
|
queueReadiness();
|
|
@@ -520,7 +621,7 @@ export function composeRuntime(options) {
|
|
|
520
621
|
}
|
|
521
622
|
}
|
|
522
623
|
function unprepared() {
|
|
523
|
-
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !
|
|
624
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
|
|
524
625
|
}
|
|
525
626
|
function unpreparedBy(locator, error) {
|
|
526
627
|
unpreparable.add(locator);
|
|
@@ -581,14 +682,14 @@ export function composeRuntime(options) {
|
|
|
581
682
|
pending.push({ room: locator, awaits: 'acquisition' });
|
|
582
683
|
continue;
|
|
583
684
|
}
|
|
584
|
-
if (!
|
|
685
|
+
if (!replica.indexed) {
|
|
585
686
|
pending.push({ room: locator, awaits: 'preparation' });
|
|
586
687
|
continue;
|
|
587
688
|
}
|
|
588
689
|
coverage.push({
|
|
589
690
|
room: locator,
|
|
590
691
|
generation: replica.record.transfer,
|
|
591
|
-
current: !(catching.has(locator) ||
|
|
692
|
+
current: !(catching.has(locator) || transfers.has(locator)),
|
|
592
693
|
});
|
|
593
694
|
read.push({ locator, key: replica.record.key });
|
|
594
695
|
for (const batch of replica.queue)
|
|
@@ -615,8 +716,6 @@ export function composeRuntime(options) {
|
|
|
615
716
|
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
616
717
|
}
|
|
617
718
|
function roomState(locator) {
|
|
618
|
-
if (sourceRefusals.has(locator))
|
|
619
|
-
return 'refused';
|
|
620
719
|
const listedRoom = discovered.get(locator);
|
|
621
720
|
if (roomsListed &&
|
|
622
721
|
(listedRoom === undefined ||
|
|
@@ -625,13 +724,13 @@ export function composeRuntime(options) {
|
|
|
625
724
|
return 'unadmitted';
|
|
626
725
|
if (!replicas.has(locator))
|
|
627
726
|
return 'acquiring';
|
|
628
|
-
if (!
|
|
727
|
+
if (!indexed(locator))
|
|
629
728
|
return 'preparing';
|
|
630
729
|
return 'complete';
|
|
631
730
|
}
|
|
632
731
|
function conversationCoverage(reach) {
|
|
633
732
|
const declared = reach === null || reach.conversations;
|
|
634
|
-
if (!declared || options.adapter.conversations === undefined
|
|
733
|
+
if (!declared || options.adapter.conversations === undefined)
|
|
635
734
|
return 'undeclared';
|
|
636
735
|
if (!conversations.known() || !conversations.current())
|
|
637
736
|
return 'acquiring';
|
|
@@ -646,21 +745,47 @@ export function composeRuntime(options) {
|
|
|
646
745
|
? 'connected'
|
|
647
746
|
: 'connecting';
|
|
648
747
|
}
|
|
649
|
-
function
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
748
|
+
function bytesOf(room, underway) {
|
|
749
|
+
const transfer = transfers.get(room);
|
|
750
|
+
if (transfer !== undefined) {
|
|
751
|
+
const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
|
|
752
|
+
return {
|
|
753
|
+
done: transfer.done.bytes - Math.ceil(owed),
|
|
754
|
+
total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
|
|
758
|
+
return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
|
|
759
|
+
}
|
|
760
|
+
function installationOf(coverage) {
|
|
761
|
+
const rooms = { done: 0, total: 0 };
|
|
762
|
+
const bytes = { done: 0, total: 0 };
|
|
763
|
+
let installing = 0;
|
|
764
|
+
let downloading = false;
|
|
765
|
+
for (const { room, state } of coverage) {
|
|
766
|
+
if (state === 'unadmitted')
|
|
767
|
+
continue;
|
|
768
|
+
rooms.total += 1;
|
|
769
|
+
if (state === 'complete')
|
|
770
|
+
rooms.done += 1;
|
|
771
|
+
const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
|
|
772
|
+
if (underway)
|
|
773
|
+
installing += 1;
|
|
774
|
+
if (underway && (streaming.has(room) || !arrivals.has(room)))
|
|
775
|
+
downloading = true;
|
|
776
|
+
const now = bytesOf(room, underway);
|
|
777
|
+
const earlier = underway ? startedOver.get(room) : undefined;
|
|
778
|
+
bytes.done += Math.max(now.done, earlier?.done ?? 0);
|
|
779
|
+
bytes.total += Math.max(now.total, earlier?.total ?? 0);
|
|
780
|
+
}
|
|
781
|
+
const step = installing === 0
|
|
782
|
+
? null
|
|
783
|
+
: downloading
|
|
784
|
+
? 'downloading'
|
|
785
|
+
: bytes.done < bytes.total
|
|
786
|
+
? 'writing'
|
|
787
|
+
: 'replaying';
|
|
788
|
+
return { step, rooms, bytes };
|
|
664
789
|
}
|
|
665
790
|
function readinessFor(port, reach) {
|
|
666
791
|
const required = requiredRooms(reach);
|
|
@@ -682,18 +807,17 @@ export function composeRuntime(options) {
|
|
|
682
807
|
persistence,
|
|
683
808
|
connection: connectionOf(),
|
|
684
809
|
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
? capacity
|
|
688
|
-
: port.capacity,
|
|
689
|
-
refused: refusedIn(required.rooms),
|
|
810
|
+
space: full ? 'full' : port.space,
|
|
811
|
+
installation: installationOf(coverage),
|
|
690
812
|
lost: storageLoss !== null,
|
|
691
|
-
established: targets[port.target]?.established === true
|
|
813
|
+
established: targets[port.target]?.established === true ||
|
|
814
|
+
(usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
|
|
692
815
|
});
|
|
693
816
|
}
|
|
694
817
|
function establish(name) {
|
|
695
818
|
targets = { ...targets, [name]: { established: true } };
|
|
696
819
|
queueReadiness();
|
|
820
|
+
probeEachMinute();
|
|
697
821
|
void durably((local) => local.fact('targets', targets)).catch(() => {
|
|
698
822
|
journal.record(`target record failed ${name}`);
|
|
699
823
|
});
|
|
@@ -731,87 +855,56 @@ export function composeRuntime(options) {
|
|
|
731
855
|
readinessWatches.delete(watch);
|
|
732
856
|
};
|
|
733
857
|
}
|
|
734
|
-
function
|
|
735
|
-
|
|
736
|
-
for (const { path, content } of page.entries)
|
|
737
|
-
bytes += (path.length + (content?.length ?? 0)) * 2;
|
|
738
|
-
return bytes;
|
|
739
|
-
}
|
|
740
|
-
function exceeded(error) {
|
|
741
|
-
return (typeof error === 'object' &&
|
|
742
|
-
error !== null &&
|
|
743
|
-
error.name === 'QuotaExceededError');
|
|
744
|
-
}
|
|
745
|
-
async function estimated() {
|
|
746
|
-
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
747
|
-
return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
|
|
748
|
-
}
|
|
749
|
-
function pause(locator, cause) {
|
|
750
|
-
paused.add(locator);
|
|
751
|
-
capacity = cause;
|
|
752
|
-
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
753
|
-
queueReadiness();
|
|
754
|
-
if (rechecking !== null)
|
|
858
|
+
function filled() {
|
|
859
|
+
if (full || closed)
|
|
755
860
|
return;
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
861
|
+
full = true;
|
|
862
|
+
journal.record('space full');
|
|
863
|
+
queueReadiness();
|
|
864
|
+
probeEachMinute();
|
|
760
865
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
return;
|
|
765
|
-
const available = await estimated().catch(() => null);
|
|
766
|
-
if (available === null || available < (held.needed ?? 0))
|
|
767
|
-
return;
|
|
768
|
-
if (held.cause === 'storage-exceeded' &&
|
|
769
|
-
(held.available === null || available <= held.available))
|
|
770
|
-
return;
|
|
771
|
-
resume();
|
|
866
|
+
function installedTarget() {
|
|
867
|
+
return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
|
|
868
|
+
Object.values(targets).some((target) => target.established));
|
|
772
869
|
}
|
|
773
|
-
function
|
|
774
|
-
if (
|
|
775
|
-
clearInterval(rechecking);
|
|
776
|
-
rechecking = null;
|
|
777
|
-
if (paused.size === 0 && capacity === null)
|
|
870
|
+
function probeEachMinute() {
|
|
871
|
+
if (!full || probing !== null || closed || !installedTarget())
|
|
778
872
|
return;
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
if (!closed && authorization !== undefined)
|
|
784
|
-
void sync('unstreamed');
|
|
785
|
-
}
|
|
786
|
-
async function reclaimed(local) {
|
|
787
|
-
const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
|
|
788
|
-
for (const locator of unreferenced) {
|
|
789
|
-
await local.abandon(locator);
|
|
790
|
-
transfers.delete(locator);
|
|
791
|
-
}
|
|
792
|
-
if (unreferenced.length > 0)
|
|
793
|
-
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
794
|
-
return unreferenced.length > 0;
|
|
873
|
+
// 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.
|
|
874
|
+
probing = setInterval(() => {
|
|
875
|
+
void probed();
|
|
876
|
+
}, 60_000);
|
|
795
877
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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)
|
|
878
|
+
function probed() {
|
|
879
|
+
testing ??= database.ask({ kind: 'space.probe' }).then(() => {
|
|
880
|
+
testing = null;
|
|
881
|
+
spaceFreed();
|
|
805
882
|
return true;
|
|
806
|
-
|
|
807
|
-
|
|
883
|
+
}, () => {
|
|
884
|
+
testing = null;
|
|
885
|
+
return false;
|
|
886
|
+
});
|
|
887
|
+
return testing;
|
|
808
888
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
889
|
+
function spaceFreed() {
|
|
890
|
+
if (!full)
|
|
891
|
+
return;
|
|
892
|
+
full = false;
|
|
893
|
+
if (probing !== null)
|
|
894
|
+
clearInterval(probing);
|
|
895
|
+
probing = null;
|
|
896
|
+
unpreparable.clear();
|
|
897
|
+
journal.record('space freed');
|
|
898
|
+
queueReadiness();
|
|
899
|
+
if (closed || authorization === undefined)
|
|
900
|
+
return;
|
|
901
|
+
if (options.adapter.roomStream === undefined)
|
|
902
|
+
void sync('unstreamed', null);
|
|
903
|
+
else
|
|
904
|
+
void resynchronize();
|
|
812
905
|
}
|
|
813
906
|
function seedRoomHeld(room, version) {
|
|
814
|
-
return
|
|
907
|
+
return indexed(room) && reachedBy(room, version);
|
|
815
908
|
}
|
|
816
909
|
function seedHeld(at) {
|
|
817
910
|
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
@@ -934,6 +1027,10 @@ export function composeRuntime(options) {
|
|
|
934
1027
|
}
|
|
935
1028
|
function publishChangeSet(set) {
|
|
936
1029
|
applyChangeSet(set);
|
|
1030
|
+
for (const { locator, path } of set.documents) {
|
|
1031
|
+
if (path !== null)
|
|
1032
|
+
attachedFiles.get(locator)?.delete(path);
|
|
1033
|
+
}
|
|
937
1034
|
if (set.rooms.length > 0 ||
|
|
938
1035
|
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
939
1036
|
emitRooms();
|
|
@@ -952,6 +1049,8 @@ export function composeRuntime(options) {
|
|
|
952
1049
|
}
|
|
953
1050
|
async function refuse(reason, provider = null) {
|
|
954
1051
|
discoveryPaused = true;
|
|
1052
|
+
inFlight.abort();
|
|
1053
|
+
inFlight = new AbortController();
|
|
955
1054
|
try {
|
|
956
1055
|
if (reason === 'revoked-authorization') {
|
|
957
1056
|
await exclusive(async () => {
|
|
@@ -959,27 +1058,24 @@ export function composeRuntime(options) {
|
|
|
959
1058
|
const records = [...replicas].map(([locator, held]) => ({
|
|
960
1059
|
locator,
|
|
961
1060
|
held,
|
|
962
|
-
record: { ...held.record, model: revoked
|
|
1061
|
+
record: { ...held.record, model: revoked },
|
|
963
1062
|
}));
|
|
1063
|
+
let answer = null;
|
|
964
1064
|
try {
|
|
965
1065
|
const local = await openLocal();
|
|
966
|
-
await local.commit({
|
|
1066
|
+
answer = await local.commit({
|
|
967
1067
|
rooms: records.map(({ locator, record }) => ({
|
|
968
1068
|
locator,
|
|
969
1069
|
key: record.key,
|
|
970
1070
|
record: storedRecord(record),
|
|
971
|
-
|
|
1071
|
+
composed: composedOf(composed(record)),
|
|
972
1072
|
})),
|
|
973
1073
|
});
|
|
974
1074
|
}
|
|
975
1075
|
catch {
|
|
976
1076
|
journal.record('model revocation persistence failed');
|
|
977
1077
|
}
|
|
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
|
-
}
|
|
1078
|
+
const keys = new Set([...models.keys(), ...rooms.keys()]);
|
|
983
1079
|
for (const key of keys) {
|
|
984
1080
|
invalidModels.add(key);
|
|
985
1081
|
models.set(key, revoked);
|
|
@@ -989,8 +1085,8 @@ export function composeRuntime(options) {
|
|
|
989
1085
|
for (const { locator, held, record } of records) {
|
|
990
1086
|
if (replicas.get(locator) !== held)
|
|
991
1087
|
continue;
|
|
992
|
-
|
|
993
|
-
replicas.set(locator, { ...held, record });
|
|
1088
|
+
const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
|
|
1089
|
+
replicas.set(locator, { ...held, record, indexed });
|
|
994
1090
|
}
|
|
995
1091
|
for (const locator of [...rooms.keys()]) {
|
|
996
1092
|
const room = rooms.get(locator);
|
|
@@ -1034,15 +1130,15 @@ export function composeRuntime(options) {
|
|
|
1034
1130
|
? null
|
|
1035
1131
|
: {
|
|
1036
1132
|
roots: selection.roots,
|
|
1037
|
-
model: modelTypes(
|
|
1133
|
+
model: modelTypes(builtIn),
|
|
1038
1134
|
},
|
|
1039
1135
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1040
1136
|
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
1041
1137
|
synchronizationAvailable,
|
|
1042
|
-
refusal: scopedRefusals.get(room.locator) ??
|
|
1138
|
+
refusal: scopedRefusals.get(room.locator) ?? null,
|
|
1043
1139
|
});
|
|
1044
1140
|
}
|
|
1045
|
-
async function applyDiscovery(found, started,
|
|
1141
|
+
async function applyDiscovery(found, started, left) {
|
|
1046
1142
|
const lockedBefore = lockedRooms();
|
|
1047
1143
|
for (const room of found) {
|
|
1048
1144
|
const fence = fences.get(room.locator);
|
|
@@ -1056,18 +1152,11 @@ export function composeRuntime(options) {
|
|
|
1056
1152
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
1057
1153
|
for (const locator of listedLocators)
|
|
1058
1154
|
scopedRefusals.delete(locator);
|
|
1059
|
-
if (
|
|
1155
|
+
if (left === undefined)
|
|
1060
1156
|
discovered.clear();
|
|
1061
|
-
else
|
|
1062
|
-
for (const locator of covered)
|
|
1063
|
-
discovered.delete(locator);
|
|
1064
1157
|
for (const room of listed)
|
|
1065
1158
|
discovered.set(room.locator, room);
|
|
1066
|
-
|
|
1067
|
-
if (options.adapter.exhaustiveDiscovery === true)
|
|
1068
|
-
await loseRooms(unlisted);
|
|
1069
|
-
else
|
|
1070
|
-
unlistedKept(unlisted);
|
|
1159
|
+
await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
|
|
1071
1160
|
for (const room of listed) {
|
|
1072
1161
|
const held = replicas.get(room.locator)?.record;
|
|
1073
1162
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -1098,15 +1187,6 @@ export function composeRuntime(options) {
|
|
|
1098
1187
|
]),
|
|
1099
1188
|
].filter((locator) => !listed.has(locator));
|
|
1100
1189
|
}
|
|
1101
|
-
function unlistedKept(unlisted) {
|
|
1102
|
-
for (const locator of unlisted) {
|
|
1103
|
-
const held = replicas.get(locator)?.record;
|
|
1104
|
-
if (held !== undefined)
|
|
1105
|
-
list(held, false);
|
|
1106
|
-
else
|
|
1107
|
-
rooms.delete(locator);
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
1190
|
function fence(locators) {
|
|
1111
1191
|
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1112
1192
|
const removed = fresh.map((locator) => ({
|
|
@@ -1123,25 +1203,19 @@ export function composeRuntime(options) {
|
|
|
1123
1203
|
queuesWithoutReplica.delete(locator);
|
|
1124
1204
|
rooms.delete(locator);
|
|
1125
1205
|
roomDrafts.delete(locator);
|
|
1126
|
-
|
|
1206
|
+
attachedFiles.delete(locator);
|
|
1207
|
+
untransferred(locator);
|
|
1208
|
+
startedOver.delete(locator);
|
|
1127
1209
|
catching.delete(locator);
|
|
1128
1210
|
discovered.delete(locator);
|
|
1129
|
-
announced.delete(locator);
|
|
1130
1211
|
unreadable.delete(locator);
|
|
1131
1212
|
stale.delete(locator);
|
|
1132
1213
|
repairable.delete(locator);
|
|
1133
1214
|
unpreparable.delete(locator);
|
|
1134
|
-
paused.delete(locator);
|
|
1135
|
-
prepared.delete(locator);
|
|
1136
1215
|
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
|
-
}
|
|
1216
|
+
models.delete(locator);
|
|
1217
|
+
liveModels.delete(locator);
|
|
1218
|
+
invalidModels.delete(locator);
|
|
1145
1219
|
journal.record(`room lost ${locator}`);
|
|
1146
1220
|
}
|
|
1147
1221
|
if (fresh.length === 0)
|
|
@@ -1189,7 +1263,14 @@ export function composeRuntime(options) {
|
|
|
1189
1263
|
});
|
|
1190
1264
|
await eraseFenced(fresh);
|
|
1191
1265
|
}
|
|
1192
|
-
function
|
|
1266
|
+
function retyped(persisted) {
|
|
1267
|
+
if (persisted.status === 'unavailable')
|
|
1268
|
+
return persisted;
|
|
1269
|
+
const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
|
|
1270
|
+
return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
|
|
1271
|
+
}
|
|
1272
|
+
function restored(key, held) {
|
|
1273
|
+
const persisted = retyped(held);
|
|
1193
1274
|
const prior = models.get(key);
|
|
1194
1275
|
if (prior !== undefined &&
|
|
1195
1276
|
prior.status !== 'unavailable' &&
|
|
@@ -1220,8 +1301,9 @@ export function composeRuntime(options) {
|
|
|
1220
1301
|
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1221
1302
|
targets = held.targets;
|
|
1222
1303
|
transfers.clear();
|
|
1223
|
-
|
|
1224
|
-
|
|
1304
|
+
arrivals.clear();
|
|
1305
|
+
streaming.clear();
|
|
1306
|
+
startedOver.clear();
|
|
1225
1307
|
limits = held.providerLimits;
|
|
1226
1308
|
capabilities = held.capabilities;
|
|
1227
1309
|
member = held.member;
|
|
@@ -1231,7 +1313,7 @@ export function composeRuntime(options) {
|
|
|
1231
1313
|
presentations.clear();
|
|
1232
1314
|
for (const record of held.presentation)
|
|
1233
1315
|
presentations.set(record.app, record.value);
|
|
1234
|
-
conversations.hydrate(heldConversations, chatDrafts);
|
|
1316
|
+
conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
|
|
1235
1317
|
pendings.clear();
|
|
1236
1318
|
for (const queue of queues) {
|
|
1237
1319
|
for (const { batch, state } of queue.pending) {
|
|
@@ -1239,7 +1321,7 @@ export function composeRuntime(options) {
|
|
|
1239
1321
|
}
|
|
1240
1322
|
}
|
|
1241
1323
|
replicas.clear();
|
|
1242
|
-
|
|
1324
|
+
reopened.clear();
|
|
1243
1325
|
queuesWithoutReplica.clear();
|
|
1244
1326
|
for (const [locator, queue] of queuedBatches) {
|
|
1245
1327
|
if (!held.rooms.has(locator))
|
|
@@ -1248,39 +1330,40 @@ export function composeRuntime(options) {
|
|
|
1248
1330
|
records.forEach((stored, index) => {
|
|
1249
1331
|
if (fences.has(stored.locator))
|
|
1250
1332
|
return;
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
const record = { ...stored, model, selectionModel };
|
|
1333
|
+
const record = { ...stored, model: restored(stored.locator, stored.model) };
|
|
1334
|
+
reopened.add(record.locator);
|
|
1254
1335
|
replicas.set(record.locator, {
|
|
1255
1336
|
record,
|
|
1256
1337
|
queue: queuedBatches.get(record.locator) ?? [],
|
|
1257
1338
|
overlaid: held.overlaid.get(record.key) ?? new Set(),
|
|
1258
1339
|
count: counts[index] ?? 0,
|
|
1259
1340
|
confirmed: 0,
|
|
1341
|
+
indexed: !held.unindexed.has(record.key),
|
|
1260
1342
|
});
|
|
1261
|
-
if (!held.unprepared.has(record.key))
|
|
1262
|
-
prepared.add(record.locator);
|
|
1263
1343
|
const existing = rooms.get(record.locator);
|
|
1264
1344
|
if (existing === undefined)
|
|
1265
|
-
list(record,
|
|
1345
|
+
list(record, record.roomVersion !== null);
|
|
1266
1346
|
else {
|
|
1267
1347
|
rooms.set(record.locator, {
|
|
1268
1348
|
...existing,
|
|
1269
1349
|
roomVersion: record.roomVersion,
|
|
1270
1350
|
operations: record.operations,
|
|
1271
1351
|
replica: record.replica,
|
|
1272
|
-
model: modelTypes(model),
|
|
1352
|
+
model: modelTypes(record.model),
|
|
1273
1353
|
typedSelection: record.typedSelection === null
|
|
1274
1354
|
? null
|
|
1275
|
-
: {
|
|
1276
|
-
roots: record.typedSelection.roots,
|
|
1277
|
-
model: modelTypes(selectionModel),
|
|
1278
|
-
},
|
|
1355
|
+
: { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
|
|
1279
1356
|
});
|
|
1280
1357
|
}
|
|
1281
1358
|
});
|
|
1282
|
-
|
|
1283
|
-
|
|
1359
|
+
try {
|
|
1360
|
+
for (const key of held.orphans)
|
|
1361
|
+
await opened.release(key);
|
|
1362
|
+
}
|
|
1363
|
+
catch (error) {
|
|
1364
|
+
if (!refusedForSpace(error))
|
|
1365
|
+
throw error;
|
|
1366
|
+
}
|
|
1284
1367
|
if (held.erasing || held.orphans.length > 0)
|
|
1285
1368
|
queueMicrotask(sweep);
|
|
1286
1369
|
return {
|
|
@@ -1305,7 +1388,7 @@ export function composeRuntime(options) {
|
|
|
1305
1388
|
async function open() {
|
|
1306
1389
|
if (closed)
|
|
1307
1390
|
throw new Error('runtime closed');
|
|
1308
|
-
const opened = openRuntimeStore(isFenced,
|
|
1391
|
+
const opened = openRuntimeStore(isFenced, database);
|
|
1309
1392
|
const interrupted = await opened.fences();
|
|
1310
1393
|
for (const locator of interrupted) {
|
|
1311
1394
|
fences.set(locator, { at: discoveries, erased: false });
|
|
@@ -1351,10 +1434,29 @@ export function composeRuntime(options) {
|
|
|
1351
1434
|
return opening;
|
|
1352
1435
|
}
|
|
1353
1436
|
function exclusive(work) {
|
|
1437
|
+
gathering = null;
|
|
1354
1438
|
const next = mutations.then(work, work);
|
|
1355
1439
|
mutations = next.then(() => undefined, () => undefined);
|
|
1356
1440
|
return next;
|
|
1357
1441
|
}
|
|
1442
|
+
function gathered(key, input, merge, work) {
|
|
1443
|
+
const tail = gathering;
|
|
1444
|
+
if (tail !== null && tail.key === key) {
|
|
1445
|
+
const merged = merge(tail.input, input);
|
|
1446
|
+
if (merged !== null) {
|
|
1447
|
+
tail.input = merged;
|
|
1448
|
+
return tail.done;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
const entry = { key, input, done: Promise.resolve() };
|
|
1452
|
+
entry.done = exclusive(() => {
|
|
1453
|
+
if (gathering === entry)
|
|
1454
|
+
gathering = null;
|
|
1455
|
+
return work(entry.input);
|
|
1456
|
+
});
|
|
1457
|
+
gathering = entry;
|
|
1458
|
+
return entry.done;
|
|
1459
|
+
}
|
|
1358
1460
|
function withdrawable(locator) {
|
|
1359
1461
|
return (batch) => {
|
|
1360
1462
|
const state = pendings.get(pendingKey(locator, batch.id));
|
|
@@ -1436,26 +1538,14 @@ export function composeRuntime(options) {
|
|
|
1436
1538
|
},
|
|
1437
1539
|
};
|
|
1438
1540
|
}
|
|
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))
|
|
1541
|
+
function committedRoom(locator, answer) {
|
|
1542
|
+
const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
|
|
1543
|
+
if (!indexed)
|
|
1454
1544
|
prepare([locator]);
|
|
1455
|
-
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
|
|
1545
|
+
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
|
|
1456
1546
|
}
|
|
1457
1547
|
function replayed(record, confirmedLayer, queue) {
|
|
1458
|
-
return stepped(replaying(confirmedLayer, queue, record.locator,
|
|
1548
|
+
return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
|
|
1459
1549
|
}
|
|
1460
1550
|
async function overlayDiff(record, before, visible, touched) {
|
|
1461
1551
|
const kept = before !== undefined && before.record.key === record.key
|
|
@@ -1475,8 +1565,6 @@ export function composeRuntime(options) {
|
|
|
1475
1565
|
function unpairedIn(locator, batch) {
|
|
1476
1566
|
if (!queueIn(locator).some((held) => held.id === batch.id))
|
|
1477
1567
|
return false;
|
|
1478
|
-
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
1479
|
-
return false;
|
|
1480
1568
|
return !leavingIn(batch, locator) || withdrawable(locator)(batch);
|
|
1481
1569
|
}
|
|
1482
1570
|
async function together(local, settlements) {
|
|
@@ -1518,7 +1606,7 @@ export function composeRuntime(options) {
|
|
|
1518
1606
|
const confirmedLayer = overrides === undefined
|
|
1519
1607
|
? read
|
|
1520
1608
|
: await stepped(slicingWith(read, overrides.set, overrides.removed));
|
|
1521
|
-
const composition =
|
|
1609
|
+
const composition = composed(record);
|
|
1522
1610
|
const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
|
|
1523
1611
|
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1524
1612
|
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
@@ -1561,6 +1649,7 @@ export function composeRuntime(options) {
|
|
|
1561
1649
|
key: record.key,
|
|
1562
1650
|
record: storedRecord(record),
|
|
1563
1651
|
roots: record.typedSelection?.roots ?? [],
|
|
1652
|
+
composed: composedOf(composition),
|
|
1564
1653
|
...settling.commit,
|
|
1565
1654
|
...(overrides === undefined
|
|
1566
1655
|
? {}
|
|
@@ -1602,14 +1691,14 @@ export function composeRuntime(options) {
|
|
|
1602
1691
|
const confirmedChanged = settling.commit?.published !== undefined ||
|
|
1603
1692
|
settling.commit?.refreshed === true ||
|
|
1604
1693
|
overrides !== undefined;
|
|
1605
|
-
|
|
1606
|
-
const { count } = committedRoom(locator, record, answer);
|
|
1694
|
+
const { count, indexed } = committedRoom(locator, answer);
|
|
1607
1695
|
replicas.set(locator, {
|
|
1608
1696
|
record,
|
|
1609
1697
|
queue: kept,
|
|
1610
1698
|
overlaid: touched,
|
|
1611
1699
|
count,
|
|
1612
1700
|
confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
|
|
1701
|
+
indexed,
|
|
1613
1702
|
});
|
|
1614
1703
|
return mergeChangeSets([
|
|
1615
1704
|
changeSet(locator, {
|
|
@@ -1624,103 +1713,96 @@ export function composeRuntime(options) {
|
|
|
1624
1713
|
};
|
|
1625
1714
|
}
|
|
1626
1715
|
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
|
-
}
|
|
1716
|
+
const model = record.model;
|
|
1717
|
+
models.set(locator, model);
|
|
1718
|
+
invalidModels.delete(locator);
|
|
1719
|
+
if (model.status === 'current')
|
|
1720
|
+
liveModels.set(locator, JSON.stringify(model.snapshot));
|
|
1721
|
+
else if (model.status === 'unavailable')
|
|
1722
|
+
liveModels.delete(locator);
|
|
1639
1723
|
}
|
|
1640
1724
|
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
|
-
};
|
|
1725
|
+
const answer = answers?.collection;
|
|
1726
|
+
if (answer === undefined)
|
|
1727
|
+
return held;
|
|
1728
|
+
const { state, refused } = attachedModel(answer, held.collection, roomTypes);
|
|
1729
|
+
if (refused !== null) {
|
|
1730
|
+
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
1731
|
+
journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
|
|
1732
|
+
}
|
|
1733
|
+
models.set(locator, state);
|
|
1734
|
+
return { collection: state };
|
|
1656
1735
|
}
|
|
1657
|
-
function publish(local, room,
|
|
1658
|
-
|
|
1736
|
+
function publish(local, room, reached, landing, live) {
|
|
1737
|
+
const landed = exclusive(async () => {
|
|
1659
1738
|
if (!live()) {
|
|
1660
1739
|
caughtUpTo(room.locator);
|
|
1661
1740
|
return true;
|
|
1662
1741
|
}
|
|
1663
1742
|
const prior = replicas.get(room.locator);
|
|
1664
|
-
if (landing.
|
|
1665
|
-
(
|
|
1666
|
-
|
|
1743
|
+
if (!stands(room.locator, landing.from)) {
|
|
1744
|
+
if (landing.kind === 'refreshed') {
|
|
1745
|
+
await local.unrefresh(reached.key);
|
|
1746
|
+
return false;
|
|
1747
|
+
}
|
|
1748
|
+
caughtUpTo(room.locator);
|
|
1749
|
+
await abandoned(local, room.locator);
|
|
1667
1750
|
return false;
|
|
1668
1751
|
}
|
|
1752
|
+
const record = landing.kind === 'refreshed' && prior !== undefined
|
|
1753
|
+
? {
|
|
1754
|
+
...prior.record,
|
|
1755
|
+
roomVersion: reached.roomVersion,
|
|
1756
|
+
...(landing.remodelled ? { model: reached.model } : {}),
|
|
1757
|
+
}
|
|
1758
|
+
: reached;
|
|
1669
1759
|
adoptModels(room.locator, record);
|
|
1670
1760
|
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
1761
|
let set;
|
|
1677
1762
|
try {
|
|
1678
|
-
set = await settle(local, room.locator, prior, record, queue
|
|
1763
|
+
set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
|
|
1679
1764
|
? {
|
|
1680
1765
|
room: true,
|
|
1681
1766
|
view: 'refreshed',
|
|
1682
|
-
commit: { refreshed: true
|
|
1767
|
+
commit: { refreshed: true },
|
|
1683
1768
|
}
|
|
1684
1769
|
: {
|
|
1685
1770
|
room: true,
|
|
1686
1771
|
view: landing.refreshed ? 'refreshed' : 'confirmed',
|
|
1687
1772
|
commit: {
|
|
1688
1773
|
published: { from: prior?.record.key ?? null },
|
|
1689
|
-
composition: compositionOf(record),
|
|
1690
1774
|
...(landing.refreshed ? { refreshed: true } : {}),
|
|
1691
1775
|
},
|
|
1692
1776
|
});
|
|
1693
1777
|
}
|
|
1694
1778
|
catch (error) {
|
|
1695
|
-
if (!
|
|
1779
|
+
if (!refusedForSpace(error))
|
|
1696
1780
|
throw error;
|
|
1697
|
-
await exceededOn(room.locator, null);
|
|
1698
1781
|
caughtUpTo(room.locator);
|
|
1699
1782
|
return true;
|
|
1700
1783
|
}
|
|
1701
1784
|
if (landing.kind === 'bootstrapped') {
|
|
1702
|
-
|
|
1785
|
+
untransferred(room.locator);
|
|
1786
|
+
startedOver.delete(room.locator);
|
|
1703
1787
|
catching.delete(room.locator);
|
|
1704
|
-
|
|
1788
|
+
if (prior !== undefined && prior.record.key !== record.key)
|
|
1789
|
+
sweep();
|
|
1790
|
+
name(room.locator);
|
|
1705
1791
|
}
|
|
1706
|
-
for (const batch of accepted)
|
|
1707
|
-
pendings.delete(pendingKey(room.locator, batch.id));
|
|
1708
1792
|
stale.delete(room.locator);
|
|
1709
1793
|
unpreparable.delete(room.locator);
|
|
1710
1794
|
if (unreadable.delete(room.locator))
|
|
1711
1795
|
scopedRefusals.delete(room.locator);
|
|
1712
|
-
sourceRefusals.delete(room.locator);
|
|
1713
|
-
declarationRefusals.delete(room.locator);
|
|
1714
1796
|
list(room, true);
|
|
1715
1797
|
publishChangeSet(set);
|
|
1716
1798
|
await unpair(local);
|
|
1717
1799
|
moveTo({ ...status, ...attention() });
|
|
1718
|
-
|
|
1719
|
-
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1800
|
+
if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1720
1801
|
submitLater(room.locator);
|
|
1721
1802
|
}
|
|
1722
1803
|
return true;
|
|
1723
1804
|
});
|
|
1805
|
+
return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
|
|
1724
1806
|
}
|
|
1725
1807
|
async function savedRoom(locator) {
|
|
1726
1808
|
const local = await openLocal();
|
|
@@ -1745,12 +1827,7 @@ export function composeRuntime(options) {
|
|
|
1745
1827
|
return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
|
|
1746
1828
|
}
|
|
1747
1829
|
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
|
-
}
|
|
1830
|
+
const read = readSavedParts(named);
|
|
1754
1831
|
const target = await savedRoom(locator);
|
|
1755
1832
|
const saved = [];
|
|
1756
1833
|
const files = [];
|
|
@@ -1759,7 +1836,7 @@ export function composeRuntime(options) {
|
|
|
1759
1836
|
for (const { path, parts } of read) {
|
|
1760
1837
|
const file = parts.file === undefined
|
|
1761
1838
|
? undefined
|
|
1762
|
-
: await heldFile(parts.file.blob, binaryBase(visible, path));
|
|
1839
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
|
|
1763
1840
|
if (parts.file !== undefined && file !== undefined) {
|
|
1764
1841
|
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1765
1842
|
}
|
|
@@ -1794,13 +1871,14 @@ export function composeRuntime(options) {
|
|
|
1794
1871
|
const referring = removed.length === 0
|
|
1795
1872
|
? []
|
|
1796
1873
|
: (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
|
|
1797
|
-
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
|
|
1874
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
|
|
1798
1875
|
const { visible } = await replayed(record, confirmedLayer, replica.queue);
|
|
1799
|
-
const composition =
|
|
1876
|
+
const composition = composed(record);
|
|
1800
1877
|
return {
|
|
1801
1878
|
record,
|
|
1802
1879
|
confirmed: confirmedLayer,
|
|
1803
1880
|
visible,
|
|
1881
|
+
folders: new Set(await local.folders(record.key, 'visible', paths)),
|
|
1804
1882
|
*visibleOver(queue) {
|
|
1805
1883
|
return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
|
|
1806
1884
|
},
|
|
@@ -1812,13 +1890,7 @@ export function composeRuntime(options) {
|
|
|
1812
1890
|
return false;
|
|
1813
1891
|
const local = await openLocal();
|
|
1814
1892
|
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)));
|
|
1893
|
+
return local.typedWithin(record.key, 'visible', roots, false);
|
|
1822
1894
|
}
|
|
1823
1895
|
function textDiffers(left, right) {
|
|
1824
1896
|
return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
|
|
@@ -1829,18 +1901,6 @@ export function composeRuntime(options) {
|
|
|
1829
1901
|
return undefined;
|
|
1830
1902
|
return entry.binary !== null ? null : entry.content;
|
|
1831
1903
|
}
|
|
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
1904
|
function lookupOf(local, key, view) {
|
|
1845
1905
|
return async (paths) => {
|
|
1846
1906
|
if (paths.length === 0)
|
|
@@ -1857,37 +1917,6 @@ export function composeRuntime(options) {
|
|
|
1857
1917
|
const read = parseFrontmatter(entry.content ?? '');
|
|
1858
1918
|
return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
|
|
1859
1919
|
}
|
|
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
1920
|
function overriddenLookup(lookup, overrides) {
|
|
1892
1921
|
return async (paths) => {
|
|
1893
1922
|
const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
|
|
@@ -1905,9 +1934,8 @@ export function composeRuntime(options) {
|
|
|
1905
1934
|
}
|
|
1906
1935
|
function validatorOf(record, confirmedLayer) {
|
|
1907
1936
|
return async (resulting, touched) => {
|
|
1908
|
-
const composition = compositionFor(record);
|
|
1909
1937
|
const local = await openLocal();
|
|
1910
|
-
const
|
|
1938
|
+
const changes = [];
|
|
1911
1939
|
const overrides = new Map();
|
|
1912
1940
|
const pace = paced();
|
|
1913
1941
|
for (const path of new Set([...confirmedLayer.held, ...touched])) {
|
|
@@ -1918,19 +1946,13 @@ export function composeRuntime(options) {
|
|
|
1918
1946
|
continue;
|
|
1919
1947
|
overrides.set(path, now);
|
|
1920
1948
|
if (textOf(was) !== textOf(now))
|
|
1921
|
-
|
|
1949
|
+
changes.push({ path, was: textOf(was), now: textOf(now) });
|
|
1922
1950
|
}
|
|
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);
|
|
1951
|
+
return validatedNeighbourhood(changes, compositionFor(record), {
|
|
1952
|
+
lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
|
|
1953
|
+
texts: textsOf(local, record.key, 'confirmed'),
|
|
1954
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
1955
|
+
});
|
|
1934
1956
|
};
|
|
1935
1957
|
}
|
|
1936
1958
|
function textsOf(local, key, view) {
|
|
@@ -1954,6 +1976,7 @@ export function composeRuntime(options) {
|
|
|
1954
1976
|
operations: room.operations,
|
|
1955
1977
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1956
1978
|
visible: saving.visible,
|
|
1979
|
+
folders: saving.folders,
|
|
1957
1980
|
queued: queuedDocuments(replica.queue),
|
|
1958
1981
|
unmodeled: await unmodeledIn(record),
|
|
1959
1982
|
validate: validatorOf(record, saving.confirmed),
|
|
@@ -1974,6 +1997,7 @@ export function composeRuntime(options) {
|
|
|
1974
1997
|
locator,
|
|
1975
1998
|
key: saving.record.key,
|
|
1976
1999
|
roots: saving.record.typedSelection?.roots ?? [],
|
|
2000
|
+
composed: composedOf(composed(saving.record)),
|
|
1977
2001
|
overlay: diff.overlay,
|
|
1978
2002
|
});
|
|
1979
2003
|
}
|
|
@@ -2005,7 +2029,7 @@ export function composeRuntime(options) {
|
|
|
2005
2029
|
}
|
|
2006
2030
|
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
2007
2031
|
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,
|
|
2032
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
|
|
2009
2033
|
if (changed.length > 0)
|
|
2010
2034
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2011
2035
|
const target = replicas.get(destination);
|
|
@@ -2117,6 +2141,8 @@ export function composeRuntime(options) {
|
|
|
2117
2141
|
submitLater(locator);
|
|
2118
2142
|
}
|
|
2119
2143
|
async function committed(locator, target, saved, consumed, writtenBy, files = []) {
|
|
2144
|
+
if (full)
|
|
2145
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2120
2146
|
const into = movedInto(locator, saved);
|
|
2121
2147
|
if (into !== null) {
|
|
2122
2148
|
return committedMove(locator, into, target, saved, consumed, writtenBy, files);
|
|
@@ -2128,7 +2154,7 @@ export function composeRuntime(options) {
|
|
|
2128
2154
|
...saved.map(({ path }) => path),
|
|
2129
2155
|
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
2130
2156
|
]);
|
|
2131
|
-
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path,
|
|
2157
|
+
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
|
|
2132
2158
|
if (changed.length > 0)
|
|
2133
2159
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2134
2160
|
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
|
|
@@ -2156,6 +2182,7 @@ export function composeRuntime(options) {
|
|
|
2156
2182
|
providerLimits: limits,
|
|
2157
2183
|
handle: room.handle,
|
|
2158
2184
|
visible: saving.visible,
|
|
2185
|
+
folders: saving.folders,
|
|
2159
2186
|
changes,
|
|
2160
2187
|
bases: parts.flatMap(({ bases }) => bases),
|
|
2161
2188
|
queued: queuedDocuments(replica.queue),
|
|
@@ -2306,6 +2333,9 @@ export function composeRuntime(options) {
|
|
|
2306
2333
|
async function readHistory(locator, path, query = {}) {
|
|
2307
2334
|
if (closed || refusal !== null)
|
|
2308
2335
|
return UNCERTAIN;
|
|
2336
|
+
await firstDiscovery;
|
|
2337
|
+
if (!roomsNamed)
|
|
2338
|
+
return UNCERTAIN;
|
|
2309
2339
|
try {
|
|
2310
2340
|
await ensureAuthorization();
|
|
2311
2341
|
}
|
|
@@ -2395,14 +2425,12 @@ export function composeRuntime(options) {
|
|
|
2395
2425
|
return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
|
|
2396
2426
|
};
|
|
2397
2427
|
}
|
|
2398
|
-
function binaryRoom(request) {
|
|
2399
|
-
if (request.kind === 'path')
|
|
2400
|
-
return request.locator;
|
|
2401
|
-
return options.adapter.referrerScope(request.referrer)?.locator ?? null;
|
|
2402
|
-
}
|
|
2403
2428
|
async function transferBinary(request, transfer, answer) {
|
|
2404
2429
|
if (closed || refusal !== null)
|
|
2405
2430
|
return UNCERTAIN;
|
|
2431
|
+
await firstDiscovery;
|
|
2432
|
+
if (!roomsNamed)
|
|
2433
|
+
return UNCERTAIN;
|
|
2406
2434
|
try {
|
|
2407
2435
|
await ensureAuthorization();
|
|
2408
2436
|
}
|
|
@@ -2412,16 +2440,13 @@ export function composeRuntime(options) {
|
|
|
2412
2440
|
if (closed || authorization === undefined || refusal !== null)
|
|
2413
2441
|
return UNCERTAIN;
|
|
2414
2442
|
const since = fencings;
|
|
2415
|
-
journal.record(request.
|
|
2416
|
-
? `transfer binary ${request.locator} ${request.path}`
|
|
2417
|
-
: `transfer binary ${request.fileRef}`);
|
|
2443
|
+
journal.record(`transfer binary ${request.locator} ${request.path}`);
|
|
2418
2444
|
try {
|
|
2419
2445
|
const result = await transfer(authorization);
|
|
2420
2446
|
const settled = result;
|
|
2421
2447
|
journal.record(`outcome binary ${settled.kind}`);
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
return failure({ kind: 'room-not-readable', locator });
|
|
2448
|
+
if (fencedSince(request.locator, since))
|
|
2449
|
+
return failure({ kind: 'room-not-readable', locator: request.locator });
|
|
2425
2450
|
if (settled.kind === 'scoped-refusal') {
|
|
2426
2451
|
const room = replicas.get(settled.locator)?.record;
|
|
2427
2452
|
if (room !== undefined)
|
|
@@ -2436,23 +2461,27 @@ export function composeRuntime(options) {
|
|
|
2436
2461
|
return UNCERTAIN;
|
|
2437
2462
|
}
|
|
2438
2463
|
}
|
|
2464
|
+
function fetchBinary(request, name) {
|
|
2465
|
+
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
2466
|
+
}
|
|
2439
2467
|
async function readBinary(request, name) {
|
|
2440
2468
|
if (closed)
|
|
2441
2469
|
return UNCERTAIN;
|
|
2442
2470
|
const held = await keptFiles.find(name);
|
|
2443
2471
|
if (held !== null)
|
|
2444
2472
|
return held;
|
|
2445
|
-
return
|
|
2473
|
+
return fetchBinary(request, name);
|
|
2446
2474
|
}
|
|
2447
2475
|
async function binaryPath(locator, path, revision) {
|
|
2448
2476
|
if (revision !== undefined)
|
|
2449
2477
|
return { kind: 'path', locator, path, revision };
|
|
2450
2478
|
const replica = replicas.get(locator);
|
|
2451
|
-
if (replica === undefined)
|
|
2452
|
-
return null;
|
|
2453
2479
|
const local = await openLocal();
|
|
2454
|
-
const [held] = await local.entries(replica.record.key, 'visible', [path]);
|
|
2455
|
-
|
|
2480
|
+
const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
|
|
2481
|
+
const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
|
|
2482
|
+
if (attached !== undefined)
|
|
2483
|
+
return { kind: 'path', locator, path, revision: attached.revision };
|
|
2484
|
+
if (replica === undefined || held?.binary === undefined || held.binary === null)
|
|
2456
2485
|
return null;
|
|
2457
2486
|
if (held.binary.state === 'pending')
|
|
2458
2487
|
return { kind: 'pending', localId: held.binary.localId };
|
|
@@ -2468,7 +2497,7 @@ export function composeRuntime(options) {
|
|
|
2468
2497
|
const holds = (at) => {
|
|
2469
2498
|
const kept = confirmedLayer.binaries[at];
|
|
2470
2499
|
return (kept?.state === 'accepted' &&
|
|
2471
|
-
kept.
|
|
2500
|
+
kept.sha256 === record.sha256 &&
|
|
2472
2501
|
confirmedLayer.revisions[at] === record.revision);
|
|
2473
2502
|
};
|
|
2474
2503
|
let confirmedAt = path;
|
|
@@ -2482,58 +2511,93 @@ export function composeRuntime(options) {
|
|
|
2482
2511
|
locator,
|
|
2483
2512
|
path: confirmedAt,
|
|
2484
2513
|
revision: record.revision,
|
|
2485
|
-
fileRef: record.fileRef,
|
|
2486
2514
|
};
|
|
2487
2515
|
}
|
|
2488
|
-
async function
|
|
2516
|
+
async function askedBinary(locator, path, revision) {
|
|
2489
2517
|
await ready;
|
|
2490
2518
|
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2491
|
-
|
|
2519
|
+
return drafted !== undefined && drafted.localId === revision
|
|
2492
2520
|
? { kind: 'pending', localId: revision }
|
|
2493
|
-
:
|
|
2521
|
+
: binaryPath(locator, path, revision);
|
|
2522
|
+
}
|
|
2523
|
+
function pendingBinary(locator, path, localId) {
|
|
2524
|
+
const facts = queuedFacts(locator, path, localId);
|
|
2525
|
+
return facts === null
|
|
2526
|
+
? absentBinary(path)
|
|
2527
|
+
: {
|
|
2528
|
+
kind: 'binary',
|
|
2529
|
+
file: localId,
|
|
2530
|
+
byteLength: facts.byteLength,
|
|
2531
|
+
mediaType: facts.mediaType,
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
async function readBinaryPath(locator, path, revision) {
|
|
2535
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2494
2536
|
if (asked === null)
|
|
2495
2537
|
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
|
-
}
|
|
2538
|
+
if (asked.kind === 'pending')
|
|
2539
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2507
2540
|
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
2508
2541
|
}
|
|
2542
|
+
async function heldMediaType(locator, path) {
|
|
2543
|
+
const replica = replicas.get(locator);
|
|
2544
|
+
const [held] = replica === undefined
|
|
2545
|
+
? []
|
|
2546
|
+
: await (await openLocal()).entries(replica.record.key, 'visible', [path]);
|
|
2547
|
+
return held === undefined
|
|
2548
|
+
? attachedFiles.get(locator)?.get(path)?.mediaType
|
|
2549
|
+
: held.binary?.mediaType;
|
|
2550
|
+
}
|
|
2551
|
+
function linkOf(asked, held) {
|
|
2552
|
+
return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
|
|
2553
|
+
}
|
|
2509
2554
|
async function linkBinaryPath(locator, path, revision) {
|
|
2510
2555
|
await ready;
|
|
2511
2556
|
const asked = await binaryPath(locator, path, revision);
|
|
2512
2557
|
if (asked === null || asked.kind === 'pending')
|
|
2513
2558
|
return absentBinary(path);
|
|
2514
|
-
return
|
|
2559
|
+
return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
|
|
2515
2560
|
}
|
|
2516
|
-
async function
|
|
2561
|
+
async function showBinaryPath(locator, path, revision) {
|
|
2562
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2563
|
+
if (asked === null)
|
|
2564
|
+
return absentBinary(path);
|
|
2565
|
+
if (asked.kind === 'pending')
|
|
2566
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2567
|
+
if (closed)
|
|
2568
|
+
return UNCERTAIN;
|
|
2569
|
+
const name = { locator, path: asked.path, revision: asked.revision };
|
|
2570
|
+
const kept = await keptFiles.find(name);
|
|
2571
|
+
if (kept !== null)
|
|
2572
|
+
return kept;
|
|
2573
|
+
const mediaType = await heldMediaType(locator, path);
|
|
2574
|
+
if (mediaType?.startsWith('image/') === true)
|
|
2575
|
+
return fetchBinary(asked, name);
|
|
2576
|
+
return linkOf(asked, revision === undefined ? mediaType : undefined);
|
|
2577
|
+
}
|
|
2578
|
+
async function attachBinary(locator, file) {
|
|
2517
2579
|
await ready;
|
|
2518
|
-
|
|
2519
|
-
if (
|
|
2520
|
-
return
|
|
2521
|
-
|
|
2580
|
+
await firstDiscovery;
|
|
2581
|
+
if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
|
|
2582
|
+
return OFFLINE;
|
|
2583
|
+
const request = await attachedFile(locator, file);
|
|
2584
|
+
return transferBinary({ locator, path: file.name }, async (held) => {
|
|
2585
|
+
const result = await options.adapter.attach(held, request);
|
|
2586
|
+
if (result.kind === 'attached') {
|
|
2587
|
+
const landed = attachedFiles.get(locator) ?? new Map();
|
|
2588
|
+
landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
|
|
2589
|
+
attachedFiles.set(locator, landed);
|
|
2590
|
+
}
|
|
2591
|
+
return result;
|
|
2592
|
+
}, attachAnswer);
|
|
2522
2593
|
}
|
|
2523
2594
|
function readPart(file, part) {
|
|
2524
|
-
return
|
|
2595
|
+
return database.ask({ kind: 'file.part', key: file, part });
|
|
2525
2596
|
}
|
|
2526
2597
|
async function cleanUp() {
|
|
2527
|
-
const freed = await
|
|
2598
|
+
const freed = await database.ask({ kind: 'opened.clean' });
|
|
2528
2599
|
return freed + ((await options.shell.cleanUp?.()) ?? 0);
|
|
2529
2600
|
}
|
|
2530
|
-
function declaredAgents() {
|
|
2531
|
-
for (const capability of capabilities) {
|
|
2532
|
-
if (capability.conversationAgents !== undefined)
|
|
2533
|
-
return capability.conversationAgents;
|
|
2534
|
-
}
|
|
2535
|
-
return undefined;
|
|
2536
|
-
}
|
|
2537
2601
|
function declaredAction(name) {
|
|
2538
2602
|
for (const capability of capabilities) {
|
|
2539
2603
|
for (const action of capability.actions ?? []) {
|
|
@@ -2585,6 +2649,24 @@ export function composeRuntime(options) {
|
|
|
2585
2649
|
return UNCERTAIN;
|
|
2586
2650
|
}
|
|
2587
2651
|
}
|
|
2652
|
+
async function readConversationHistory(request) {
|
|
2653
|
+
await ready;
|
|
2654
|
+
const stream = options.adapter.conversations;
|
|
2655
|
+
if (closed || refusal !== null || stream?.history === undefined)
|
|
2656
|
+
return UNCERTAIN;
|
|
2657
|
+
journal.record('transfer conversation history');
|
|
2658
|
+
try {
|
|
2659
|
+
const result = await stream.history(request);
|
|
2660
|
+
if (result.kind === 'revoked-authorization')
|
|
2661
|
+
await refuse('revoked-authorization');
|
|
2662
|
+
journal.record(`outcome conversation history ${result.kind}`);
|
|
2663
|
+
return calledResult(result);
|
|
2664
|
+
}
|
|
2665
|
+
catch {
|
|
2666
|
+
journal.record('outcome conversation history transport-uncertainty');
|
|
2667
|
+
return UNCERTAIN;
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2588
2670
|
function unsettled(result) {
|
|
2589
2671
|
return result.kind !== 'action' && result.retryable;
|
|
2590
2672
|
}
|
|
@@ -2656,169 +2738,222 @@ export function composeRuntime(options) {
|
|
|
2656
2738
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
2657
2739
|
!options.adapter.reached(locator, roomVersion, written));
|
|
2658
2740
|
}
|
|
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) {
|
|
2741
|
+
function refreshingOf(local, key, composition, liveness, replaying = null) {
|
|
2742
|
+
const write = replaying === null ? exclusive : finishingWrite;
|
|
2755
2743
|
return {
|
|
2744
|
+
turn() {
|
|
2745
|
+
if (replaying !== null)
|
|
2746
|
+
installing(replaying);
|
|
2747
|
+
},
|
|
2756
2748
|
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2757
2749
|
async stage(page) {
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
const last =
|
|
2761
|
-
const
|
|
2750
|
+
for (let written = 0;;) {
|
|
2751
|
+
const end = batchEnd(page.entries, written);
|
|
2752
|
+
const last = end === page.entries.length;
|
|
2753
|
+
const entries = page.entries.slice(written, end);
|
|
2754
|
+
const staged = await write(async () => {
|
|
2762
2755
|
if (!liveness())
|
|
2763
2756
|
return false;
|
|
2764
|
-
await local.refresh(key, last
|
|
2757
|
+
await timed(stageBudget, () => local.refresh(key, composition, last
|
|
2765
2758
|
? { ...page, entries }
|
|
2766
|
-
: { entries, removals: [], folders: [], folderRemovals: [] });
|
|
2759
|
+
: { entries, removals: [], folders: [], folderRemovals: [] }));
|
|
2767
2760
|
return true;
|
|
2768
2761
|
});
|
|
2769
2762
|
if (!staged)
|
|
2770
2763
|
return false;
|
|
2764
|
+
if (last)
|
|
2765
|
+
return true;
|
|
2766
|
+
written = end;
|
|
2771
2767
|
}
|
|
2772
|
-
return true;
|
|
2773
2768
|
},
|
|
2774
2769
|
};
|
|
2775
2770
|
}
|
|
2776
|
-
function
|
|
2777
|
-
|
|
2771
|
+
function batchEnd(entries, from) {
|
|
2772
|
+
let end = from;
|
|
2778
2773
|
let spent = 0;
|
|
2779
|
-
|
|
2780
|
-
const size = (
|
|
2781
|
-
if (spent + size >
|
|
2782
|
-
|
|
2783
|
-
spent = 0;
|
|
2784
|
-
}
|
|
2785
|
-
batches.at(-1).push(entry);
|
|
2774
|
+
while (end < entries.length) {
|
|
2775
|
+
const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
|
|
2776
|
+
if (spent + size > stageBudget.bytes && end > from)
|
|
2777
|
+
break;
|
|
2786
2778
|
spent += size;
|
|
2779
|
+
end += 1;
|
|
2780
|
+
}
|
|
2781
|
+
return end;
|
|
2782
|
+
}
|
|
2783
|
+
async function timed(budget, work) {
|
|
2784
|
+
const started = performance.now();
|
|
2785
|
+
const done = await work();
|
|
2786
|
+
const spent = performance.now() - started;
|
|
2787
|
+
if (spent < budget.aim / 2)
|
|
2788
|
+
budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
|
|
2789
|
+
else if (spent > budget.aim * 2) {
|
|
2790
|
+
budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
|
|
2791
|
+
}
|
|
2792
|
+
return done;
|
|
2793
|
+
}
|
|
2794
|
+
function installWrite(work) {
|
|
2795
|
+
return installTurn(installTurns.starting, work);
|
|
2796
|
+
}
|
|
2797
|
+
function finishingWrite(work) {
|
|
2798
|
+
return installTurn(installTurns.finishing, work);
|
|
2799
|
+
}
|
|
2800
|
+
function installTurn(queue, work) {
|
|
2801
|
+
return new Promise((resolve, reject) => {
|
|
2802
|
+
queue.push(() => void exclusive(work)
|
|
2803
|
+
.then(resolve, reject)
|
|
2804
|
+
.finally(() => {
|
|
2805
|
+
installTurns.busy = false;
|
|
2806
|
+
nextInstallTurn();
|
|
2807
|
+
}));
|
|
2808
|
+
nextInstallTurn();
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2811
|
+
function nextInstallTurn() {
|
|
2812
|
+
if (installTurns.busy || installTurns.ahead > 0)
|
|
2813
|
+
return;
|
|
2814
|
+
const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
|
|
2815
|
+
if (turn === undefined)
|
|
2816
|
+
return;
|
|
2817
|
+
installTurns.busy = true;
|
|
2818
|
+
turn();
|
|
2819
|
+
}
|
|
2820
|
+
async function ahead(work) {
|
|
2821
|
+
let taken = false;
|
|
2822
|
+
const hold = {
|
|
2823
|
+
take() {
|
|
2824
|
+
if (taken)
|
|
2825
|
+
return;
|
|
2826
|
+
taken = true;
|
|
2827
|
+
installTurns.ahead += 1;
|
|
2828
|
+
},
|
|
2829
|
+
release() {
|
|
2830
|
+
if (!taken)
|
|
2831
|
+
return;
|
|
2832
|
+
taken = false;
|
|
2833
|
+
installTurns.ahead -= 1;
|
|
2834
|
+
resumeInstall();
|
|
2835
|
+
},
|
|
2836
|
+
};
|
|
2837
|
+
hold.take();
|
|
2838
|
+
try {
|
|
2839
|
+
return await work(hold);
|
|
2840
|
+
}
|
|
2841
|
+
finally {
|
|
2842
|
+
hold.release();
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
function resumeInstall() {
|
|
2846
|
+
if (installTurns.ahead > 0)
|
|
2847
|
+
return;
|
|
2848
|
+
void yielded().then(nextInstallTurn);
|
|
2849
|
+
}
|
|
2850
|
+
async function heldStream() {
|
|
2851
|
+
while (streamsFree === 0)
|
|
2852
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
2853
|
+
streamsFree -= 1;
|
|
2854
|
+
return once(() => {
|
|
2855
|
+
streamsFree += 1;
|
|
2856
|
+
});
|
|
2857
|
+
}
|
|
2858
|
+
async function unqueued() {
|
|
2859
|
+
while (queuedText >= QUEUED_TEXT) {
|
|
2860
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
function once(release) {
|
|
2864
|
+
let released = false;
|
|
2865
|
+
return () => {
|
|
2866
|
+
if (released)
|
|
2867
|
+
return;
|
|
2868
|
+
released = true;
|
|
2869
|
+
release();
|
|
2870
|
+
for (const admit of installWaiters.splice(0))
|
|
2871
|
+
admit();
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
function installing(locator) {
|
|
2875
|
+
installs.add(locator);
|
|
2876
|
+
queueReadiness();
|
|
2877
|
+
}
|
|
2878
|
+
function untransferred(locator) {
|
|
2879
|
+
transfers.delete(locator);
|
|
2880
|
+
arrivals.delete(locator);
|
|
2881
|
+
streaming.delete(locator);
|
|
2882
|
+
underived.delete(locator);
|
|
2883
|
+
unindexed.delete(locator);
|
|
2884
|
+
}
|
|
2885
|
+
function setBack(locator) {
|
|
2886
|
+
const now = bytesOf(locator, true);
|
|
2887
|
+
const earlier = startedOver.get(locator);
|
|
2888
|
+
startedOver.set(locator, {
|
|
2889
|
+
done: Math.max(now.done, earlier?.done ?? 0),
|
|
2890
|
+
total: Math.max(now.total, earlier?.total ?? 0),
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
function owedIndexes(locator, bytes, text) {
|
|
2894
|
+
if (bytes <= 0 || text <= 0)
|
|
2895
|
+
return;
|
|
2896
|
+
unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
|
|
2897
|
+
}
|
|
2898
|
+
function built(locator, { done, text }) {
|
|
2899
|
+
const owed = unindexed.get(locator) ?? [];
|
|
2900
|
+
let left = text;
|
|
2901
|
+
while (!done && left > 0 && owed.length > 0) {
|
|
2902
|
+
const [first] = owed;
|
|
2903
|
+
if (left >= first.text) {
|
|
2904
|
+
left -= first.text;
|
|
2905
|
+
owed.shift();
|
|
2906
|
+
continue;
|
|
2907
|
+
}
|
|
2908
|
+
owed[0] = {
|
|
2909
|
+
bytes: (first.bytes * (first.text - left)) / first.text,
|
|
2910
|
+
text: first.text - left,
|
|
2911
|
+
};
|
|
2912
|
+
left = 0;
|
|
2787
2913
|
}
|
|
2788
|
-
|
|
2914
|
+
if (done || owed.length === 0)
|
|
2915
|
+
unindexed.delete(locator);
|
|
2916
|
+
queueReadiness();
|
|
2789
2917
|
}
|
|
2790
|
-
|
|
2918
|
+
function installed(locator) {
|
|
2919
|
+
if (installs.delete(locator))
|
|
2920
|
+
queueReadiness();
|
|
2921
|
+
}
|
|
2922
|
+
async function pullRefresh(authorized, held, liveness, hold, refresh) {
|
|
2791
2923
|
if (!liveness())
|
|
2792
2924
|
return unconfirmed;
|
|
2793
|
-
const turn = generation;
|
|
2794
2925
|
const { record } = held;
|
|
2795
|
-
const
|
|
2926
|
+
const from = standingOf(record.locator);
|
|
2796
2927
|
const local = await openLocal();
|
|
2797
2928
|
try {
|
|
2798
2929
|
await exclusive(() => local.unrefresh(record.key));
|
|
2799
|
-
const result = await advancement(
|
|
2930
|
+
const result = await advancement(async (authorization, request) => {
|
|
2931
|
+
hold.release();
|
|
2932
|
+
try {
|
|
2933
|
+
return await refresh(authorization, request);
|
|
2934
|
+
}
|
|
2935
|
+
finally {
|
|
2936
|
+
hold.take();
|
|
2937
|
+
}
|
|
2938
|
+
}, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
|
|
2800
2939
|
if (result.kind !== 'advanced') {
|
|
2801
2940
|
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2802
2941
|
return result;
|
|
2803
2942
|
}
|
|
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
2943
|
journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
|
|
2813
|
-
return {
|
|
2944
|
+
return {
|
|
2945
|
+
kind: 'advanced',
|
|
2946
|
+
record: result.record,
|
|
2947
|
+
from,
|
|
2948
|
+
remodelled: result.record.model !== record.model,
|
|
2949
|
+
};
|
|
2814
2950
|
}
|
|
2815
2951
|
catch (error) {
|
|
2952
|
+
if (refusedForSpace(error))
|
|
2953
|
+
return unconfirmed;
|
|
2816
2954
|
journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
|
|
2817
2955
|
throw error;
|
|
2818
2956
|
}
|
|
2819
|
-
finally {
|
|
2820
|
-
reads.finished(record.locator, turn);
|
|
2821
|
-
}
|
|
2822
2957
|
}
|
|
2823
2958
|
const heldContents = new Map();
|
|
2824
2959
|
function heldContentOf(local, key) {
|
|
@@ -2836,63 +2971,115 @@ export function composeRuntime(options) {
|
|
|
2836
2971
|
heldContents.set(key, made);
|
|
2837
2972
|
return made;
|
|
2838
2973
|
}
|
|
2974
|
+
async function abandoned(local, locator) {
|
|
2975
|
+
await local.abandon(locator);
|
|
2976
|
+
setBack(locator);
|
|
2977
|
+
untransferred(locator);
|
|
2978
|
+
liveQueries.recover();
|
|
2979
|
+
sweep();
|
|
2980
|
+
}
|
|
2839
2981
|
function staging(local, liveness) {
|
|
2982
|
+
const derived = async (locator, key, composition) => {
|
|
2983
|
+
underived.delete(locator);
|
|
2984
|
+
for (;;) {
|
|
2985
|
+
if (!liveness())
|
|
2986
|
+
return false;
|
|
2987
|
+
const done = await finishingWrite(async () => liveness()
|
|
2988
|
+
? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
|
|
2989
|
+
: null);
|
|
2990
|
+
if (done === null)
|
|
2991
|
+
return false;
|
|
2992
|
+
built(locator, done);
|
|
2993
|
+
if (done.done)
|
|
2994
|
+
return true;
|
|
2995
|
+
}
|
|
2996
|
+
};
|
|
2840
2997
|
return {
|
|
2841
2998
|
held: (locator) => transfers.get(locator) ?? null,
|
|
2999
|
+
async turn(locator, step) {
|
|
3000
|
+
installing(locator);
|
|
3001
|
+
return step === 'downloading' ? heldStream() : () => { };
|
|
3002
|
+
},
|
|
3003
|
+
unqueued,
|
|
3004
|
+
queued(text) {
|
|
3005
|
+
queuedText += text;
|
|
3006
|
+
return once(() => {
|
|
3007
|
+
queuedText -= text;
|
|
3008
|
+
});
|
|
3009
|
+
},
|
|
3010
|
+
arriving(locator) {
|
|
3011
|
+
let ended = false;
|
|
3012
|
+
return {
|
|
3013
|
+
at(bytes) {
|
|
3014
|
+
if (ended)
|
|
3015
|
+
return;
|
|
3016
|
+
arrivals.set(locator, bytes);
|
|
3017
|
+
streaming.add(locator);
|
|
3018
|
+
const now = performance.now();
|
|
3019
|
+
if (now - arrivalReported < ARRIVAL_REPORT_MS)
|
|
3020
|
+
return;
|
|
3021
|
+
arrivalReported = now;
|
|
3022
|
+
queueReadiness();
|
|
3023
|
+
},
|
|
3024
|
+
end() {
|
|
3025
|
+
if (ended)
|
|
3026
|
+
return;
|
|
3027
|
+
ended = true;
|
|
3028
|
+
if (streaming.delete(locator))
|
|
3029
|
+
queueReadiness();
|
|
3030
|
+
},
|
|
3031
|
+
};
|
|
3032
|
+
},
|
|
3033
|
+
streamed: options.adapter.roomStream !== undefined,
|
|
2842
3034
|
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)
|
|
3035
|
+
const { locator } = record;
|
|
3036
|
+
installing(locator);
|
|
3037
|
+
if (!liveness())
|
|
3038
|
+
return null;
|
|
3039
|
+
const key = await installWrite(async () => {
|
|
3040
|
+
if (!liveness())
|
|
2864
3041
|
return null;
|
|
2865
|
-
|
|
2866
|
-
}
|
|
2867
|
-
|
|
3042
|
+
return local.stage(record, composition, page);
|
|
3043
|
+
});
|
|
3044
|
+
if (key === null || !liveness())
|
|
3045
|
+
return null;
|
|
3046
|
+
const arrived = !transfers.has(locator);
|
|
3047
|
+
const prior = transfers.get(locator)?.done.bytes ?? 0;
|
|
3048
|
+
const held = { ...record, key };
|
|
3049
|
+
transfers.set(locator, held);
|
|
3050
|
+
if (arrived)
|
|
3051
|
+
liveQueries.recover();
|
|
3052
|
+
installing(locator);
|
|
2868
3053
|
liveQueries.recover();
|
|
2869
|
-
|
|
3054
|
+
const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
|
|
3055
|
+
owedIndexes(locator, record.done.bytes - prior, text);
|
|
3056
|
+
const owed = text + (underived.get(locator) ?? 0);
|
|
3057
|
+
underived.set(locator, owed);
|
|
3058
|
+
if (owed < deriveBudget.bytes)
|
|
3059
|
+
return held;
|
|
3060
|
+
return (await derived(locator, key, composition)) ? held : null;
|
|
2870
3061
|
},
|
|
2871
|
-
|
|
3062
|
+
derive: derived,
|
|
3063
|
+
async refreshing(locator, key, composition) {
|
|
2872
3064
|
await exclusive(() => local.unrefresh(key));
|
|
2873
|
-
return refreshingOf(local, key, liveness);
|
|
3065
|
+
return refreshingOf(local, key, composition, liveness, locator);
|
|
2874
3066
|
},
|
|
2875
3067
|
abandon: (locator) => exclusive(async () => {
|
|
2876
|
-
if (
|
|
2877
|
-
|
|
2878
|
-
await local.abandon(locator);
|
|
2879
|
-
transfers.delete(locator);
|
|
2880
|
-
liveQueries.recover();
|
|
2881
|
-
sweep();
|
|
3068
|
+
if (liveness())
|
|
3069
|
+
await abandoned(local, locator);
|
|
2882
3070
|
}),
|
|
2883
|
-
setAside:
|
|
3071
|
+
setAside: async (locator) => {
|
|
2884
3072
|
if (!liveness())
|
|
2885
3073
|
return;
|
|
2886
|
-
|
|
2887
|
-
|
|
3074
|
+
setBack(locator);
|
|
3075
|
+
untransferred(locator);
|
|
2888
3076
|
liveQueries.recover();
|
|
2889
|
-
}
|
|
3077
|
+
},
|
|
2890
3078
|
release: (key) => exclusive(async () => {
|
|
2891
3079
|
await local.release(key);
|
|
2892
3080
|
sweep();
|
|
2893
3081
|
}),
|
|
2894
3082
|
content: (key) => heldContentOf(local, key),
|
|
2895
|
-
validate: (record, view) => validatedRoom(local, record, view, null),
|
|
2896
3083
|
};
|
|
2897
3084
|
}
|
|
2898
3085
|
function reusable(room, local) {
|
|
@@ -2904,30 +3091,28 @@ export function composeRuntime(options) {
|
|
|
2904
3091
|
async function pullBootstrap(authorized, room, local, liveness) {
|
|
2905
3092
|
if (!liveness())
|
|
2906
3093
|
return unconfirmed;
|
|
2907
|
-
const turn = generation;
|
|
2908
3094
|
if (replicas.has(room.locator)) {
|
|
2909
3095
|
catching.add(room.locator);
|
|
2910
3096
|
liveQueries.recover();
|
|
2911
3097
|
}
|
|
2912
3098
|
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));
|
|
3099
|
+
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
3100
|
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
2918
3101
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
2919
|
-
if (result.kind !== 'staged')
|
|
3102
|
+
if (result.kind !== 'staged') {
|
|
2920
3103
|
caughtUpTo(room.locator);
|
|
3104
|
+
installed(room.locator);
|
|
3105
|
+
}
|
|
2921
3106
|
return result;
|
|
2922
3107
|
}
|
|
2923
3108
|
catch (error) {
|
|
2924
|
-
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
2925
3109
|
caughtUpTo(room.locator);
|
|
3110
|
+
installed(room.locator);
|
|
3111
|
+
if (refusedForSpace(error))
|
|
3112
|
+
return unconfirmed;
|
|
3113
|
+
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
2926
3114
|
throw error;
|
|
2927
3115
|
}
|
|
2928
|
-
finally {
|
|
2929
|
-
reads.finished(room.locator, turn);
|
|
2930
|
-
}
|
|
2931
3116
|
}
|
|
2932
3117
|
function caughtUpTo(locator) {
|
|
2933
3118
|
if (catching.delete(locator))
|
|
@@ -2935,72 +3120,82 @@ export function composeRuntime(options) {
|
|
|
2935
3120
|
}
|
|
2936
3121
|
function behind(room) {
|
|
2937
3122
|
const held = replicas.get(room.locator)?.record.roomVersion;
|
|
2938
|
-
return
|
|
2939
|
-
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
3123
|
+
return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
|
|
2940
3124
|
}
|
|
2941
|
-
|
|
2942
|
-
const
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
return
|
|
3125
|
+
function standingOf(locator) {
|
|
3126
|
+
const held = replicas.get(locator);
|
|
3127
|
+
return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
|
|
3128
|
+
}
|
|
3129
|
+
function stands(locator, from) {
|
|
3130
|
+
const now = standingOf(locator);
|
|
3131
|
+
return now.key === from.key && now.confirmed === from.confirmed;
|
|
2948
3132
|
}
|
|
2949
|
-
async function
|
|
2950
|
-
|
|
3133
|
+
async function advance(room, local, authorized, running) {
|
|
3134
|
+
const live = () => running() && !full;
|
|
3135
|
+
if (!live())
|
|
2951
3136
|
return unconfirmed;
|
|
2952
3137
|
const held = replicas.get(room.locator);
|
|
2953
|
-
if (held !== undefined) {
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
emitRooms();
|
|
2965
|
-
}
|
|
2966
|
-
return null;
|
|
2967
|
-
}
|
|
2968
|
-
if (refreshed.kind === 'advanced') {
|
|
2969
|
-
const landing = { kind: 'refreshed', base: refreshed.base };
|
|
2970
|
-
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
2971
|
-
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
2972
|
-
const reached = now !== undefined &&
|
|
2973
|
-
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
2974
|
-
return reached ? null : advance(room, local, authorized, live);
|
|
2975
|
-
}
|
|
2976
|
-
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
2977
|
-
return null;
|
|
2978
|
-
}
|
|
2979
|
-
if (refreshed.kind !== 'beyond-horizon')
|
|
3138
|
+
if (held !== undefined && options.adapter.roomStream !== undefined) {
|
|
3139
|
+
stale.delete(room.locator);
|
|
3140
|
+
name(room.locator);
|
|
3141
|
+
return null;
|
|
3142
|
+
}
|
|
3143
|
+
const refresh = reads.refresh;
|
|
3144
|
+
if (held !== undefined && refresh !== undefined) {
|
|
3145
|
+
const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
|
|
3146
|
+
if (refreshed === 'behind')
|
|
3147
|
+
return advance(room, local, authorized, live);
|
|
3148
|
+
if (refreshed !== 'beyond-horizon')
|
|
2980
3149
|
return refreshed;
|
|
2981
|
-
if (
|
|
2982
|
-
owed.add(room.locator);
|
|
2983
|
-
liveQueries.recover();
|
|
2984
|
-
return unconfirmed;
|
|
2985
|
-
}
|
|
2986
|
-
if (reloadsDue.get(room.locator) === generation)
|
|
3150
|
+
if (reloadsDue.has(room.locator))
|
|
2987
3151
|
return unconfirmed;
|
|
2988
3152
|
}
|
|
3153
|
+
const from = standingOf(room.locator);
|
|
2989
3154
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
3155
|
+
if (bootstrapped.kind !== 'staged')
|
|
3156
|
+
return live() ? bootstrapped : unconfirmed;
|
|
3157
|
+
const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
|
|
3158
|
+
if (await publish(local, room, bootstrapped.record, landing, live)) {
|
|
3159
|
+
return live() ? null : unconfirmed;
|
|
3160
|
+
}
|
|
3161
|
+
return reachedBy(room.locator, bootstrapped.record.roomVersion)
|
|
3162
|
+
? null
|
|
3163
|
+
: advance(room, local, authorized, live);
|
|
3164
|
+
}
|
|
3165
|
+
async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
|
|
3166
|
+
const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
|
|
2990
3167
|
if (!live())
|
|
2991
3168
|
return unconfirmed;
|
|
2992
|
-
if (
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
3169
|
+
if (refreshed.kind === 'still-current') {
|
|
3170
|
+
stale.delete(room.locator);
|
|
3171
|
+
if (unreadable.delete(room.locator)) {
|
|
3172
|
+
scopedRefusals.delete(room.locator);
|
|
3173
|
+
list(room, true);
|
|
3174
|
+
emitRooms();
|
|
3175
|
+
}
|
|
3176
|
+
return null;
|
|
3177
|
+
}
|
|
3178
|
+
if (refreshed.kind === 'advanced') {
|
|
3179
|
+
const landing = {
|
|
3180
|
+
kind: 'refreshed',
|
|
3181
|
+
from: refreshed.from,
|
|
3182
|
+
remodelled: refreshed.remodelled,
|
|
3183
|
+
};
|
|
3184
|
+
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3185
|
+
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3186
|
+
const reached = now !== undefined &&
|
|
3187
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3188
|
+
return reached ? null : 'behind';
|
|
3189
|
+
}
|
|
3190
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3191
|
+
return null;
|
|
3192
|
+
}
|
|
3193
|
+
return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
|
|
2996
3194
|
}
|
|
2997
3195
|
async function withhold(withheld, room, local, progress) {
|
|
2998
|
-
if (withheld.kind === 'unconfirmed')
|
|
3196
|
+
if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
|
|
2999
3197
|
return;
|
|
3000
|
-
|
|
3001
|
-
stale.delete(room.locator);
|
|
3002
|
-
else
|
|
3003
|
-
stale.add(room.locator);
|
|
3198
|
+
stale.add(room.locator);
|
|
3004
3199
|
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
3005
3200
|
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
3006
3201
|
repairable.set(room.locator, reset);
|
|
@@ -3022,30 +3217,10 @@ export function composeRuntime(options) {
|
|
|
3022
3217
|
list(room, false);
|
|
3023
3218
|
emitRooms();
|
|
3024
3219
|
}
|
|
3025
|
-
else if (withheld.kind === 'refused') {
|
|
3026
|
-
refusedSource(room, withheld);
|
|
3027
|
-
}
|
|
3028
3220
|
else if (withheld.kind === 'revoked-authorization') {
|
|
3029
3221
|
await refuse('revoked-authorization');
|
|
3030
3222
|
}
|
|
3031
3223
|
}
|
|
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
3224
|
function leaving(batch, result) {
|
|
3050
3225
|
if (result.kind === 'expired')
|
|
3051
3226
|
return { batch, returned: returnedBy('expired') };
|
|
@@ -3067,8 +3242,8 @@ export function composeRuntime(options) {
|
|
|
3067
3242
|
function settling(acceptance) {
|
|
3068
3243
|
return acceptance !== null && 'apply' in acceptance;
|
|
3069
3244
|
}
|
|
3070
|
-
function
|
|
3071
|
-
return settling(acceptance) ?
|
|
3245
|
+
function failureOf(acceptance) {
|
|
3246
|
+
return settling(acceptance) ? null : acceptance;
|
|
3072
3247
|
}
|
|
3073
3248
|
async function acceptance(room, local, submitted, accepted) {
|
|
3074
3249
|
if (!continuing())
|
|
@@ -3091,34 +3266,6 @@ export function composeRuntime(options) {
|
|
|
3091
3266
|
? accepted.roomVersion
|
|
3092
3267
|
: held,
|
|
3093
3268
|
};
|
|
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
3269
|
changes = back;
|
|
3123
3270
|
}
|
|
3124
3271
|
const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
@@ -3128,8 +3275,6 @@ export function composeRuntime(options) {
|
|
|
3128
3275
|
const set = settled.apply(answer);
|
|
3129
3276
|
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3130
3277
|
unreadable.delete(room.locator);
|
|
3131
|
-
sourceRefusals.delete(room.locator);
|
|
3132
|
-
declarationRefusals.delete(room.locator);
|
|
3133
3278
|
list(room, true);
|
|
3134
3279
|
return set;
|
|
3135
3280
|
},
|
|
@@ -3147,13 +3292,9 @@ export function composeRuntime(options) {
|
|
|
3147
3292
|
const failed = await exclusive(async () => {
|
|
3148
3293
|
const adopting = await acceptance(room, local, submitted, accepted);
|
|
3149
3294
|
await adoptedTogether(local, [adopting]);
|
|
3150
|
-
return
|
|
3295
|
+
return failureOf(adopting);
|
|
3151
3296
|
});
|
|
3152
|
-
|
|
3153
|
-
return true;
|
|
3154
|
-
if (failed.kind === 'refused' && continuing())
|
|
3155
|
-
refusedSource(room, failed);
|
|
3156
|
-
return false;
|
|
3297
|
+
return failed === null;
|
|
3157
3298
|
}
|
|
3158
3299
|
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
3159
3300
|
const other = discovered.get(partner);
|
|
@@ -3167,18 +3308,10 @@ export function composeRuntime(options) {
|
|
|
3167
3308
|
...accepted.destination,
|
|
3168
3309
|
});
|
|
3169
3310
|
await adoptedTogether(local, [left, arrived]);
|
|
3170
|
-
|
|
3171
|
-
if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
|
|
3172
|
-
refusedSource(other, refusedArrival);
|
|
3173
|
-
}
|
|
3174
|
-
return refusalOf(left);
|
|
3311
|
+
return failureOf(left);
|
|
3175
3312
|
});
|
|
3176
3313
|
submitLater(partner);
|
|
3177
|
-
|
|
3178
|
-
return true;
|
|
3179
|
-
if (failed.kind === 'refused' && continuing())
|
|
3180
|
-
refusedSource(room, failed);
|
|
3181
|
-
return false;
|
|
3314
|
+
return failed === null;
|
|
3182
3315
|
}
|
|
3183
3316
|
function queueIn(locator) {
|
|
3184
3317
|
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
@@ -3301,8 +3434,6 @@ export function composeRuntime(options) {
|
|
|
3301
3434
|
if (replica === undefined || head === undefined)
|
|
3302
3435
|
return 'done';
|
|
3303
3436
|
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
3304
|
-
if (state?.accepted !== undefined)
|
|
3305
|
-
return 'done';
|
|
3306
3437
|
const partner = partnerOf(head, room.locator);
|
|
3307
3438
|
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3308
3439
|
submitLater(partner);
|
|
@@ -3416,12 +3547,12 @@ export function composeRuntime(options) {
|
|
|
3416
3547
|
}
|
|
3417
3548
|
if (result.kind === 'expired') {
|
|
3418
3549
|
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
3419
|
-
const there = other === undefined ? null : await
|
|
3550
|
+
const there = other === undefined ? null : await advanced(other, local, authorized);
|
|
3420
3551
|
if (other !== undefined && there !== null) {
|
|
3421
3552
|
await withhold(there, other, local, progress);
|
|
3422
3553
|
return 'done';
|
|
3423
3554
|
}
|
|
3424
|
-
const withheld = await
|
|
3555
|
+
const withheld = await advanced(room, local, authorized);
|
|
3425
3556
|
if (withheld !== null) {
|
|
3426
3557
|
await withhold(withheld, room, local, progress);
|
|
3427
3558
|
return 'done';
|
|
@@ -3443,8 +3574,8 @@ export function composeRuntime(options) {
|
|
|
3443
3574
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
3444
3575
|
const left = leaving(head, result);
|
|
3445
3576
|
const refreshed = async () => ({
|
|
3446
|
-
there: other === undefined ? null : await
|
|
3447
|
-
here: await
|
|
3577
|
+
there: other === undefined ? null : await advanced(other, local, authorized),
|
|
3578
|
+
here: await advanced(room, local, authorized),
|
|
3448
3579
|
});
|
|
3449
3580
|
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
3450
3581
|
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
@@ -3515,6 +3646,7 @@ export function composeRuntime(options) {
|
|
|
3515
3646
|
locator: room.locator,
|
|
3516
3647
|
key: before.record.key,
|
|
3517
3648
|
roots: before.record.typedSelection?.roots ?? [],
|
|
3649
|
+
composed: composedOf(composed(before.record)),
|
|
3518
3650
|
overlay: { set: [], cleared: [...before.overlaid] },
|
|
3519
3651
|
},
|
|
3520
3652
|
]),
|
|
@@ -3547,39 +3679,37 @@ export function composeRuntime(options) {
|
|
|
3547
3679
|
if (signal.aborted)
|
|
3548
3680
|
return;
|
|
3549
3681
|
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
|
-
});
|
|
3682
|
+
await roomTask(room.locator, (live) => reload(room, local, progress, live));
|
|
3556
3683
|
if (signal.aborted)
|
|
3557
3684
|
return;
|
|
3558
3685
|
await rest(progress);
|
|
3559
3686
|
}
|
|
3560
3687
|
}
|
|
3561
3688
|
function awaitingAnswer(locator) {
|
|
3689
|
+
answering.get(locator)?.release();
|
|
3562
3690
|
let release = () => { };
|
|
3563
|
-
const
|
|
3691
|
+
const answered = new Promise((resolve) => {
|
|
3564
3692
|
release = resolve;
|
|
3565
3693
|
});
|
|
3566
|
-
|
|
3694
|
+
const awaited = { answered, release };
|
|
3695
|
+
answering.set(locator, awaited);
|
|
3567
3696
|
return () => {
|
|
3568
|
-
if (answering.get(locator)
|
|
3697
|
+
if (answering.get(locator) === awaited)
|
|
3569
3698
|
answering.delete(locator);
|
|
3570
3699
|
release();
|
|
3571
3700
|
};
|
|
3572
3701
|
}
|
|
3573
|
-
|
|
3574
|
-
const
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3702
|
+
function declaredAsHeld(room) {
|
|
3703
|
+
const held = replicas.get(room.locator)?.record;
|
|
3704
|
+
return (held === undefined ||
|
|
3705
|
+
(JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
|
|
3706
|
+
JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
|
|
3707
|
+
JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
|
|
3708
|
+
}
|
|
3709
|
+
async function retainModel(local, room, next) {
|
|
3710
|
+
const key = room.locator;
|
|
3711
|
+
const previous = modelOf(room.locator);
|
|
3712
|
+
if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
|
|
3583
3713
|
return;
|
|
3584
3714
|
if (next.status === 'unavailable') {
|
|
3585
3715
|
invalidModels.add(key);
|
|
@@ -3591,26 +3721,13 @@ export function composeRuntime(options) {
|
|
|
3591
3721
|
const held = replicas.get(room.locator);
|
|
3592
3722
|
let changed = null;
|
|
3593
3723
|
if (held !== undefined) {
|
|
3594
|
-
const
|
|
3724
|
+
const record = {
|
|
3595
3725
|
...held.record,
|
|
3596
3726
|
operations: room.operations,
|
|
3597
3727
|
replica: room.replica,
|
|
3598
3728
|
typedSelection: room.typedSelection ?? null,
|
|
3599
|
-
|
|
3729
|
+
model: next,
|
|
3600
3730
|
};
|
|
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
3731
|
const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
|
|
3615
3732
|
const replayedLayer = await replayed(record, confirmedLayer, held.queue);
|
|
3616
3733
|
const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
|
|
@@ -3621,41 +3738,33 @@ export function composeRuntime(options) {
|
|
|
3621
3738
|
key: record.key,
|
|
3622
3739
|
record: storedRecord(record),
|
|
3623
3740
|
roots: record.typedSelection?.roots ?? [],
|
|
3624
|
-
|
|
3741
|
+
composed: composedOf(composed(record)),
|
|
3625
3742
|
overlay: diff.overlay,
|
|
3626
3743
|
},
|
|
3627
3744
|
],
|
|
3628
3745
|
});
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count });
|
|
3746
|
+
const { count, indexed } = committedRoom(room.locator, answer);
|
|
3747
|
+
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
|
|
3632
3748
|
changed = changeSet(room.locator, {
|
|
3633
3749
|
documents: answer.changed[room.locator] ?? [],
|
|
3634
3750
|
room: true,
|
|
3635
3751
|
});
|
|
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
3752
|
}
|
|
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));
|
|
3753
|
+
heldModel(room, next);
|
|
3653
3754
|
if (changed === null)
|
|
3654
3755
|
emitRooms();
|
|
3655
3756
|
else
|
|
3656
3757
|
publishChangeSet(changed);
|
|
3657
3758
|
});
|
|
3658
3759
|
}
|
|
3760
|
+
function heldModel(room, next) {
|
|
3761
|
+
models.set(room.locator, next);
|
|
3762
|
+
if (next.status === 'current') {
|
|
3763
|
+
liveModels.set(room.locator, JSON.stringify(next.snapshot));
|
|
3764
|
+
invalidModels.delete(room.locator);
|
|
3765
|
+
}
|
|
3766
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
3767
|
+
}
|
|
3659
3768
|
async function scoped(result, room, local) {
|
|
3660
3769
|
if (result.scope === 'content' && !result.retryable) {
|
|
3661
3770
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
@@ -3669,14 +3778,14 @@ export function composeRuntime(options) {
|
|
|
3669
3778
|
});
|
|
3670
3779
|
let scopedAway = room;
|
|
3671
3780
|
if (result.scope === 'model')
|
|
3672
|
-
await retainModel(local, room,
|
|
3781
|
+
await retainModel(local, room, {
|
|
3673
3782
|
status: 'unavailable',
|
|
3674
3783
|
reason: 'not-readable',
|
|
3675
3784
|
});
|
|
3676
3785
|
else if (result.scope !== 'operation') {
|
|
3677
3786
|
unreadable.add(room.locator);
|
|
3678
3787
|
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
3679
|
-
await retainModel(local, scopedAway,
|
|
3788
|
+
await retainModel(local, scopedAway, modelOf(room.locator));
|
|
3680
3789
|
}
|
|
3681
3790
|
if (!continuing())
|
|
3682
3791
|
return;
|
|
@@ -3684,6 +3793,17 @@ export function composeRuntime(options) {
|
|
|
3684
3793
|
emitRooms();
|
|
3685
3794
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
3686
3795
|
}
|
|
3796
|
+
function name(locator) {
|
|
3797
|
+
const stream = options.adapter.roomStream;
|
|
3798
|
+
const record = replicas.get(locator)?.record;
|
|
3799
|
+
const room = discovered.get(locator);
|
|
3800
|
+
if (stream === undefined || record === undefined || room === undefined)
|
|
3801
|
+
return;
|
|
3802
|
+
if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
|
|
3803
|
+
return;
|
|
3804
|
+
naming += 1;
|
|
3805
|
+
stream.hold([{ locator, roomVersion: record.roomVersion }]);
|
|
3806
|
+
}
|
|
3687
3807
|
function heldRooms(listed) {
|
|
3688
3808
|
return listed.flatMap((room) => {
|
|
3689
3809
|
const record = replicas.get(room.locator)?.record;
|
|
@@ -3701,14 +3821,19 @@ export function composeRuntime(options) {
|
|
|
3701
3821
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
3702
3822
|
});
|
|
3703
3823
|
if (attempt === 'opened') {
|
|
3824
|
+
naming += 1;
|
|
3704
3825
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
3705
3826
|
handed = true;
|
|
3706
3827
|
}
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3828
|
+
for (const room of ready) {
|
|
3829
|
+
if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
|
|
3830
|
+
if (attempt === 'authorized')
|
|
3831
|
+
name(room.locator);
|
|
3832
|
+
continue;
|
|
3833
|
+
}
|
|
3834
|
+
void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
|
|
3835
|
+
}
|
|
3836
|
+
const failed = prepared.find((room) => room.status === 'rejected');
|
|
3712
3837
|
if (failed !== undefined)
|
|
3713
3838
|
throw failed.reason;
|
|
3714
3839
|
}
|
|
@@ -3726,27 +3851,29 @@ export function composeRuntime(options) {
|
|
|
3726
3851
|
return false;
|
|
3727
3852
|
}
|
|
3728
3853
|
if (prior !== undefined)
|
|
3729
|
-
await retainModel(local, room,
|
|
3854
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3730
3855
|
if (discoveryPaused || authorization === undefined)
|
|
3731
3856
|
return false;
|
|
3732
3857
|
return room.replica.admission === 'admitted';
|
|
3733
3858
|
}
|
|
3734
3859
|
async function caughtUp(room, local, progress, live) {
|
|
3735
|
-
if (
|
|
3860
|
+
if (options.adapter.roomStream !== undefined || authorization === undefined)
|
|
3861
|
+
return;
|
|
3862
|
+
if (!live() || !behind(room))
|
|
3736
3863
|
return;
|
|
3737
3864
|
const withheld = await advance(room, local, authorization, live);
|
|
3738
3865
|
if (withheld !== null)
|
|
3739
3866
|
await withhold(withheld, room, local, progress);
|
|
3740
3867
|
}
|
|
3741
3868
|
function revoked(locator) {
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3745
|
-
});
|
|
3869
|
+
const model = modelOf(locator);
|
|
3870
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3746
3871
|
}
|
|
3747
3872
|
async function loadRoom(room, local, progress, live) {
|
|
3748
3873
|
if (!live() || authorization === undefined)
|
|
3749
3874
|
return;
|
|
3875
|
+
if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
|
|
3876
|
+
return;
|
|
3750
3877
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
3751
3878
|
await reload(room, local, progress, live);
|
|
3752
3879
|
return;
|
|
@@ -3764,21 +3891,53 @@ export function composeRuntime(options) {
|
|
|
3764
3891
|
reloadsDue.delete(room.locator);
|
|
3765
3892
|
if (!live() || authorization === undefined)
|
|
3766
3893
|
return;
|
|
3894
|
+
const from = standingOf(room.locator);
|
|
3767
3895
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
3768
|
-
if (!live())
|
|
3769
|
-
return;
|
|
3770
3896
|
if (bootstrapped.kind !== 'staged') {
|
|
3771
|
-
|
|
3897
|
+
if (live())
|
|
3898
|
+
await withhold(bootstrapped, room, local, progress);
|
|
3772
3899
|
return;
|
|
3773
3900
|
}
|
|
3774
|
-
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
3901
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
|
|
3775
3902
|
await caughtUp(room, local, progress, live);
|
|
3776
3903
|
}
|
|
3777
3904
|
finally {
|
|
3778
3905
|
caughtUpTo(room.locator);
|
|
3779
3906
|
}
|
|
3780
3907
|
}
|
|
3781
|
-
async function
|
|
3908
|
+
async function asked(authorized) {
|
|
3909
|
+
try {
|
|
3910
|
+
return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
|
|
3911
|
+
}
|
|
3912
|
+
catch {
|
|
3913
|
+
return { kind: 'transport-uncertainty' };
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
function malformed(listed) {
|
|
3917
|
+
for (const room of listed) {
|
|
3918
|
+
if (room.operations === undefined ||
|
|
3919
|
+
!['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
3920
|
+
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
3921
|
+
room.replica === undefined ||
|
|
3922
|
+
typeof room.replica.selection !== 'string' ||
|
|
3923
|
+
room.replica.selection.length === 0 ||
|
|
3924
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
3925
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
3926
|
+
return `invalid model or replica declaration ${room.locator}`;
|
|
3927
|
+
}
|
|
3928
|
+
if (!declaredSelection(room))
|
|
3929
|
+
return `invalid typed selection declaration ${room.locator}`;
|
|
3930
|
+
}
|
|
3931
|
+
return null;
|
|
3932
|
+
}
|
|
3933
|
+
async function signIn(local, signedIn) {
|
|
3934
|
+
if (JSON.stringify(signedIn) === JSON.stringify(member))
|
|
3935
|
+
return;
|
|
3936
|
+
await local.fact('member', signedIn);
|
|
3937
|
+
member = signedIn;
|
|
3938
|
+
emitMember();
|
|
3939
|
+
}
|
|
3940
|
+
async function discoverRooms(local, progress, attempt, live, found) {
|
|
3782
3941
|
if (closed || authorization === undefined) {
|
|
3783
3942
|
notifyReady();
|
|
3784
3943
|
return;
|
|
@@ -3786,13 +3945,9 @@ export function composeRuntime(options) {
|
|
|
3786
3945
|
moveTo(transferring(status));
|
|
3787
3946
|
discoveries += 1;
|
|
3788
3947
|
const started = discoveries;
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
}
|
|
3793
|
-
catch {
|
|
3794
|
-
result = { kind: 'transport-uncertainty' };
|
|
3795
|
-
}
|
|
3948
|
+
const result = found ?? (await asked(authorization));
|
|
3949
|
+
if (result.kind === 'discovery')
|
|
3950
|
+
roomsNamed = true;
|
|
3796
3951
|
if (!live()) {
|
|
3797
3952
|
progress.complete = false;
|
|
3798
3953
|
notifyReady();
|
|
@@ -3800,12 +3955,7 @@ export function composeRuntime(options) {
|
|
|
3800
3955
|
}
|
|
3801
3956
|
journal.record(`connection discover ${result.kind}`);
|
|
3802
3957
|
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
|
-
}
|
|
3958
|
+
await cachedModels(local);
|
|
3809
3959
|
progress.reachable = false;
|
|
3810
3960
|
progress.complete = false;
|
|
3811
3961
|
notifyReady();
|
|
@@ -3826,26 +3976,12 @@ export function composeRuntime(options) {
|
|
|
3826
3976
|
notifyReady();
|
|
3827
3977
|
return;
|
|
3828
3978
|
}
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
room.replica.selection.length === 0 ||
|
|
3836
|
-
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
3837
|
-
!['locked', 'open'].includes(room.replica.offline)) {
|
|
3838
|
-
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
3839
|
-
await refuse('missing-required-guarantee');
|
|
3840
|
-
notifyReady();
|
|
3841
|
-
return;
|
|
3842
|
-
}
|
|
3843
|
-
if (!declaredSelection(room)) {
|
|
3844
|
-
journal.record(`invalid typed selection declaration ${room.locator}`);
|
|
3845
|
-
await refuse('missing-required-guarantee');
|
|
3846
|
-
notifyReady();
|
|
3847
|
-
return;
|
|
3848
|
-
}
|
|
3979
|
+
const invalid = malformed(result.rooms);
|
|
3980
|
+
if (invalid !== null) {
|
|
3981
|
+
journal.record(invalid);
|
|
3982
|
+
await refuse('missing-required-guarantee');
|
|
3983
|
+
notifyReady();
|
|
3984
|
+
return;
|
|
3849
3985
|
}
|
|
3850
3986
|
if (refusal !== null) {
|
|
3851
3987
|
refusal = null;
|
|
@@ -3861,13 +3997,8 @@ export function composeRuntime(options) {
|
|
|
3861
3997
|
await local.fact('providerLimits', limits);
|
|
3862
3998
|
await local.fact('capabilities', capabilities);
|
|
3863
3999
|
}
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
await local.fact('member', signedIn);
|
|
3867
|
-
member = signedIn;
|
|
3868
|
-
emitMember();
|
|
3869
|
-
}
|
|
3870
|
-
await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
|
|
4000
|
+
await signIn(local, result.member ?? null);
|
|
4001
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
3871
4002
|
}
|
|
3872
4003
|
async function ensureAuthorization() {
|
|
3873
4004
|
if (authorization !== undefined)
|
|
@@ -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)
|
|
@@ -3914,23 +4067,19 @@ export function composeRuntime(options) {
|
|
|
3914
4067
|
void run.finally(() => working.delete(run));
|
|
3915
4068
|
return run;
|
|
3916
4069
|
}
|
|
3917
|
-
function sync(attempt) {
|
|
4070
|
+
function sync(attempt, found) {
|
|
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
|
-
|
|
3925
|
-
if (attempt === 'opened')
|
|
3926
|
-
opened = generation;
|
|
3927
|
-
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
4077
|
+
underway = attempt === 'opened' ? progress : null;
|
|
3928
4078
|
if (attempt === 'opened')
|
|
3929
4079
|
streamed = progress;
|
|
3930
4080
|
complete = false;
|
|
3931
4081
|
recorded = false;
|
|
3932
4082
|
moveTo(transferring(status));
|
|
3933
|
-
handed = false;
|
|
3934
4083
|
try {
|
|
3935
4084
|
const local = await openLocal();
|
|
3936
4085
|
if (!live())
|
|
@@ -3938,7 +4087,7 @@ export function composeRuntime(options) {
|
|
|
3938
4087
|
await ensureAuthorization();
|
|
3939
4088
|
if (!live())
|
|
3940
4089
|
return;
|
|
3941
|
-
await discoverRooms(local, progress, attempt, live);
|
|
4090
|
+
await discoverRooms(local, progress, attempt, live, found);
|
|
3942
4091
|
}
|
|
3943
4092
|
catch {
|
|
3944
4093
|
progress.complete = false;
|
|
@@ -3946,7 +4095,6 @@ export function composeRuntime(options) {
|
|
|
3946
4095
|
progress.reachable = false;
|
|
3947
4096
|
}
|
|
3948
4097
|
finally {
|
|
3949
|
-
gate.release();
|
|
3950
4098
|
notifyReady();
|
|
3951
4099
|
}
|
|
3952
4100
|
if (!live())
|
|
@@ -3958,35 +4106,53 @@ export function composeRuntime(options) {
|
|
|
3958
4106
|
if (handed || closed || progress.reachable)
|
|
3959
4107
|
return;
|
|
3960
4108
|
underway = null;
|
|
3961
|
-
opened = null;
|
|
3962
|
-
journal.record('connection reopen');
|
|
3963
4109
|
await rest(progress, live);
|
|
3964
|
-
options.adapter.roomStream?.reopen();
|
|
3965
4110
|
});
|
|
3966
4111
|
}
|
|
3967
|
-
function
|
|
4112
|
+
function resynchronize() {
|
|
3968
4113
|
return task(OPENING_LANE, async (live) => {
|
|
3969
|
-
if (!live() || !
|
|
4114
|
+
if (!live() || !announced)
|
|
3970
4115
|
return;
|
|
3971
4116
|
const progress = { complete: true, reachable: true };
|
|
3972
|
-
const gate = discover();
|
|
3973
4117
|
moveTo(transferring(status));
|
|
3974
4118
|
try {
|
|
3975
4119
|
const local = await openLocal();
|
|
3976
4120
|
if (!live())
|
|
3977
4121
|
return;
|
|
3978
|
-
await
|
|
4122
|
+
await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
|
|
4123
|
+
}
|
|
4124
|
+
catch {
|
|
4125
|
+
progress.complete = false;
|
|
4126
|
+
}
|
|
4127
|
+
if (live())
|
|
4128
|
+
await finishOpening(progress, live);
|
|
4129
|
+
});
|
|
4130
|
+
}
|
|
4131
|
+
function changeAuthority(change) {
|
|
4132
|
+
return task(OPENING_LANE, async (live) => {
|
|
4133
|
+
if (!live() || !announced)
|
|
4134
|
+
return;
|
|
4135
|
+
const progress = { complete: true, reachable: true };
|
|
4136
|
+
moveTo(transferring(status));
|
|
4137
|
+
try {
|
|
4138
|
+
const local = await openLocal();
|
|
3979
4139
|
if (!live())
|
|
3980
4140
|
return;
|
|
3981
|
-
|
|
4141
|
+
const invalid = malformed(change.rooms);
|
|
4142
|
+
if (invalid !== null) {
|
|
4143
|
+
journal.record(invalid);
|
|
4144
|
+
await refuse('missing-required-guarantee');
|
|
4145
|
+
return;
|
|
4146
|
+
}
|
|
4147
|
+
if (change.member !== undefined)
|
|
4148
|
+
await signIn(local, change.member);
|
|
4149
|
+
discoveries += 1;
|
|
4150
|
+
const listed = await applyDiscovery(change.rooms, discoveries, change.left);
|
|
4151
|
+
await synchronize(listed, local, progress, 'authorized', live);
|
|
3982
4152
|
}
|
|
3983
4153
|
catch {
|
|
3984
4154
|
progress.complete = false;
|
|
3985
4155
|
}
|
|
3986
|
-
finally {
|
|
3987
|
-
gate.release();
|
|
3988
|
-
notifyReady();
|
|
3989
|
-
}
|
|
3990
4156
|
if (!live())
|
|
3991
4157
|
return;
|
|
3992
4158
|
if (!progress.complete)
|
|
@@ -3995,13 +4161,15 @@ export function composeRuntime(options) {
|
|
|
3995
4161
|
});
|
|
3996
4162
|
}
|
|
3997
4163
|
function closedSocket(revoked, lost) {
|
|
3998
|
-
|
|
4164
|
+
notifyReady();
|
|
4165
|
+
const opened = announced;
|
|
3999
4166
|
socketOpen = false;
|
|
4167
|
+
announced = false;
|
|
4168
|
+
handed = false;
|
|
4169
|
+
naming = 0;
|
|
4000
4170
|
connectionLost();
|
|
4001
|
-
if (lost)
|
|
4002
|
-
reads.lost();
|
|
4171
|
+
if (lost)
|
|
4003
4172
|
conversations.lost();
|
|
4004
|
-
}
|
|
4005
4173
|
if (revoked) {
|
|
4006
4174
|
void refuse('revoked-authorization');
|
|
4007
4175
|
return;
|
|
@@ -4014,16 +4182,29 @@ export function composeRuntime(options) {
|
|
|
4014
4182
|
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
4015
4183
|
providerUnreachable: status.providerUnreachable || !opened,
|
|
4016
4184
|
});
|
|
4185
|
+
void closing(opened);
|
|
4186
|
+
}
|
|
4187
|
+
function closing(opened) {
|
|
4188
|
+
return task(OPENING_LANE, async (live) => {
|
|
4189
|
+
if (!live())
|
|
4190
|
+
return;
|
|
4191
|
+
const local = await openLocal();
|
|
4192
|
+
if (!opened && !socketOpen)
|
|
4193
|
+
await cachedModels(local);
|
|
4194
|
+
await rest(null, live);
|
|
4195
|
+
});
|
|
4017
4196
|
}
|
|
4018
|
-
function
|
|
4019
|
-
|
|
4197
|
+
function cachedModels(local) {
|
|
4198
|
+
return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
|
|
4199
|
+
}
|
|
4200
|
+
function caughtUpRooms(expired) {
|
|
4201
|
+
const progress = underway;
|
|
4202
|
+
underway = null;
|
|
4203
|
+
naming = Math.max(0, naming - 1);
|
|
4020
4204
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
4021
|
-
if (opened === generation)
|
|
4022
|
-
opened = null;
|
|
4023
4205
|
const reloading = progress === null ? [] : owedReloads(expired);
|
|
4024
4206
|
const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
|
|
4025
4207
|
return task(OPENING_LANE, async (live) => {
|
|
4026
|
-
underway = null;
|
|
4027
4208
|
if (!live())
|
|
4028
4209
|
return;
|
|
4029
4210
|
if (progress === null) {
|
|
@@ -4031,24 +4212,14 @@ export function composeRuntime(options) {
|
|
|
4031
4212
|
await rest(cycle, live);
|
|
4032
4213
|
return;
|
|
4033
4214
|
}
|
|
4034
|
-
await roomsCarried([...expired, ...reloading]);
|
|
4035
|
-
if (!live())
|
|
4036
|
-
return;
|
|
4037
4215
|
try {
|
|
4038
4216
|
const local = await openLocal();
|
|
4039
|
-
|
|
4217
|
+
await replacing;
|
|
4040
4218
|
if (progress.reachable) {
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
!reloading.includes(room.locator)) {
|
|
4045
|
-
await confirmModels(room, local);
|
|
4046
|
-
}
|
|
4047
|
-
}
|
|
4219
|
+
await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
|
|
4220
|
+
!expired.includes(room.locator) &&
|
|
4221
|
+
!reloading.includes(room.locator)), 'current', local);
|
|
4048
4222
|
}
|
|
4049
|
-
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
4050
|
-
if (failed !== undefined)
|
|
4051
|
-
throw failed.reason;
|
|
4052
4223
|
}
|
|
4053
4224
|
catch {
|
|
4054
4225
|
progress.complete = false;
|
|
@@ -4060,11 +4231,8 @@ export function composeRuntime(options) {
|
|
|
4060
4231
|
}
|
|
4061
4232
|
function owedReloads(expired) {
|
|
4062
4233
|
const reloading = [...discovered.values()]
|
|
4063
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
4064
|
-
!expired.includes(room.locator) &&
|
|
4065
|
-
(owed.has(room.locator) || revoked(room.locator)))
|
|
4234
|
+
.filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
|
|
4066
4235
|
.map((room) => room.locator);
|
|
4067
|
-
owed.clear();
|
|
4068
4236
|
for (const locator of reloading)
|
|
4069
4237
|
catching.add(locator);
|
|
4070
4238
|
liveQueries.recover();
|
|
@@ -4073,24 +4241,43 @@ export function composeRuntime(options) {
|
|
|
4073
4241
|
async function reloadNow(replaced, progress) {
|
|
4074
4242
|
const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
|
|
4075
4243
|
for (const room of reloaded)
|
|
4076
|
-
reloadsDue.
|
|
4244
|
+
reloadsDue.add(room.locator);
|
|
4077
4245
|
try {
|
|
4078
4246
|
const local = await openLocal();
|
|
4079
|
-
|
|
4247
|
+
for (const room of reloaded) {
|
|
4248
|
+
void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
|
|
4249
|
+
}
|
|
4080
4250
|
}
|
|
4081
4251
|
catch {
|
|
4082
4252
|
for (const room of reloaded)
|
|
4083
4253
|
reloadsDue.delete(room.locator);
|
|
4084
4254
|
progress.reachable = false;
|
|
4085
|
-
return [];
|
|
4086
4255
|
}
|
|
4087
4256
|
}
|
|
4088
|
-
async function
|
|
4089
|
-
|
|
4090
|
-
const model = modelOf(room.locator
|
|
4091
|
-
|
|
4092
|
-
|
|
4257
|
+
async function settleModels(held, status, local) {
|
|
4258
|
+
const owed = (room) => {
|
|
4259
|
+
const model = modelOf(room.locator);
|
|
4260
|
+
return model.status !== 'unavailable' && model.status !== status;
|
|
4261
|
+
};
|
|
4262
|
+
const settled = await exclusive(async () => {
|
|
4263
|
+
if (!continuing())
|
|
4264
|
+
return [];
|
|
4265
|
+
const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
|
|
4266
|
+
for (const room of unchanged) {
|
|
4267
|
+
const next = { ...modelOf(room.locator), status };
|
|
4268
|
+
const replica = replicas.get(room.locator);
|
|
4269
|
+
if (replica !== undefined)
|
|
4270
|
+
replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
|
|
4271
|
+
heldModel(room, next);
|
|
4093
4272
|
}
|
|
4273
|
+
if (unchanged.length > 0)
|
|
4274
|
+
emitRooms();
|
|
4275
|
+
return unchanged;
|
|
4276
|
+
});
|
|
4277
|
+
for (const room of held) {
|
|
4278
|
+
if (settled.includes(room) || !owed(room))
|
|
4279
|
+
continue;
|
|
4280
|
+
await retainModel(local, room, { ...modelOf(room.locator), status });
|
|
4094
4281
|
}
|
|
4095
4282
|
}
|
|
4096
4283
|
function submitLater(locator) {
|
|
@@ -4135,14 +4322,10 @@ export function composeRuntime(options) {
|
|
|
4135
4322
|
try {
|
|
4136
4323
|
const local = await openLocal();
|
|
4137
4324
|
await roomTask(locator, async (live) => {
|
|
4138
|
-
|
|
4139
|
-
if (!current() || authorization === undefined)
|
|
4325
|
+
if (!live() || authorization === undefined)
|
|
4140
4326
|
return;
|
|
4141
|
-
const withheld = await
|
|
4142
|
-
|
|
4143
|
-
abandonedOn(signal),
|
|
4144
|
-
]);
|
|
4145
|
-
if (withheld !== null && current())
|
|
4327
|
+
const withheld = await advance(room, local, authorization, live);
|
|
4328
|
+
if (withheld !== null && live())
|
|
4146
4329
|
await withhold(withheld, room, local, progress);
|
|
4147
4330
|
});
|
|
4148
4331
|
}
|
|
@@ -4197,14 +4380,11 @@ export function composeRuntime(options) {
|
|
|
4197
4380
|
progress.complete = false;
|
|
4198
4381
|
return;
|
|
4199
4382
|
}
|
|
4200
|
-
reads.applied();
|
|
4201
4383
|
submitAll();
|
|
4202
4384
|
for (const locator of stale)
|
|
4203
4385
|
if (repairable.has(locator))
|
|
4204
4386
|
repairLater(locator, {});
|
|
4205
4387
|
await conversations.reconcile();
|
|
4206
|
-
if (status.providerUnreachable)
|
|
4207
|
-
conversations.reconnected();
|
|
4208
4388
|
const reading = { complete: true, reachable: true };
|
|
4209
4389
|
await conversations.catchUp(reading);
|
|
4210
4390
|
if (progress.reachable && reading.reachable)
|
|
@@ -4230,7 +4410,7 @@ export function composeRuntime(options) {
|
|
|
4230
4410
|
if (!live())
|
|
4231
4411
|
return;
|
|
4232
4412
|
let lastCompleted = status.lastCompleted;
|
|
4233
|
-
let synced = complete && stale.size === 0 && !pending();
|
|
4413
|
+
let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
|
|
4234
4414
|
if (synced && !recorded) {
|
|
4235
4415
|
const completedAt = new Date().toISOString();
|
|
4236
4416
|
try {
|
|
@@ -4245,74 +4425,69 @@ export function composeRuntime(options) {
|
|
|
4245
4425
|
}
|
|
4246
4426
|
}
|
|
4247
4427
|
moveTo({
|
|
4248
|
-
state: pending()
|
|
4428
|
+
state: pending()
|
|
4429
|
+
? 'pending'
|
|
4430
|
+
: synced && complete && roomWork === 0 && naming === 0
|
|
4431
|
+
? 'synced'
|
|
4432
|
+
: 'syncing',
|
|
4249
4433
|
lastCompleted,
|
|
4250
|
-
providerUnreachable: !progress.reachable,
|
|
4434
|
+
providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
|
|
4251
4435
|
...attention(),
|
|
4252
4436
|
});
|
|
4253
4437
|
}
|
|
4254
|
-
function
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
}
|
|
4262
|
-
function applyRoom(locator, roomVersion) {
|
|
4263
|
-
announced.set(locator, roomVersion);
|
|
4264
|
-
return task(roomLane(locator), async (live) => {
|
|
4265
|
-
if (discovering?.generation === generation)
|
|
4266
|
-
await discovering.done;
|
|
4267
|
-
const saving = answering.get(locator);
|
|
4268
|
-
if (saving?.generation === generation)
|
|
4269
|
-
await saving.answered;
|
|
4270
|
-
if (!live() || authorization === undefined)
|
|
4271
|
-
return;
|
|
4272
|
-
const room = discovered.get(locator);
|
|
4273
|
-
const held = replicas.get(locator);
|
|
4274
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
4275
|
-
return;
|
|
4276
|
-
if (held !== undefined &&
|
|
4277
|
-
(held.record.replica.selection !== room.replica.selection ||
|
|
4278
|
-
options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
|
|
4279
|
-
return;
|
|
4280
|
-
}
|
|
4281
|
-
const during = underway?.generation === generation ? underway.progress : null;
|
|
4282
|
-
const progress = during ?? { complete: true, reachable: true };
|
|
4283
|
-
if (during === null)
|
|
4284
|
-
moveTo(transferring(status));
|
|
4438
|
+
function applyRoom(change) {
|
|
4439
|
+
const { locator } = change;
|
|
4440
|
+
if (stopping)
|
|
4441
|
+
return Promise.resolve();
|
|
4442
|
+
return inRoom(locator, async (live) => {
|
|
4443
|
+
await answering.get(locator)?.answered;
|
|
4444
|
+
const progress = underway ?? { complete: true, reachable: true };
|
|
4285
4445
|
try {
|
|
4286
4446
|
const local = await openLocal();
|
|
4287
|
-
|
|
4288
|
-
if (!live()
|
|
4289
|
-
return;
|
|
4290
|
-
const
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4447
|
+
for (;;) {
|
|
4448
|
+
if (!live())
|
|
4449
|
+
return null;
|
|
4450
|
+
const room = discovered.get(locator);
|
|
4451
|
+
const held = replicas.get(locator);
|
|
4452
|
+
if (room === undefined ||
|
|
4453
|
+
held === undefined ||
|
|
4454
|
+
room.replica.admission !== 'admitted' ||
|
|
4455
|
+
held.record.replica.selection !== room.replica.selection) {
|
|
4456
|
+
return null;
|
|
4457
|
+
}
|
|
4458
|
+
const at = held.record.roomVersion;
|
|
4459
|
+
if (options.adapter.reached(locator, at, change.roomVersion))
|
|
4460
|
+
return progress;
|
|
4461
|
+
if (!options.adapter.reached(locator, at, change.from)) {
|
|
4462
|
+
name(locator);
|
|
4463
|
+
return progress;
|
|
4464
|
+
}
|
|
4465
|
+
if (underway === null)
|
|
4466
|
+
moveTo(transferring(status));
|
|
4467
|
+
const writing = () => live() && !full;
|
|
4468
|
+
const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, () => change.read()));
|
|
4469
|
+
if (refreshed.kind !== 'advanced')
|
|
4470
|
+
return progress;
|
|
4471
|
+
const landing = {
|
|
4472
|
+
kind: 'refreshed',
|
|
4473
|
+
from: refreshed.from,
|
|
4474
|
+
remodelled: refreshed.remodelled,
|
|
4475
|
+
};
|
|
4476
|
+
if (await publish(local, room, refreshed.record, landing, writing)) {
|
|
4477
|
+
stale.delete(locator);
|
|
4478
|
+
await retainModel(local, room, modelOf(locator));
|
|
4479
|
+
return progress;
|
|
4480
|
+
}
|
|
4481
|
+
}
|
|
4294
4482
|
}
|
|
4295
4483
|
catch {
|
|
4484
|
+
if (!stale.has(locator))
|
|
4485
|
+
name(locator);
|
|
4296
4486
|
stale.add(locator);
|
|
4487
|
+
return progress;
|
|
4297
4488
|
}
|
|
4298
|
-
|
|
4299
|
-
if (during === null)
|
|
4300
|
-
await rest(progress, live);
|
|
4301
|
-
}
|
|
4302
|
-
});
|
|
4489
|
+
}).then((progress) => rest(progress), () => undefined);
|
|
4303
4490
|
}
|
|
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
4491
|
function observeStream() {
|
|
4317
4492
|
const stream = options.adapter.roomStream;
|
|
4318
4493
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -4322,29 +4497,29 @@ export function composeRuntime(options) {
|
|
|
4322
4497
|
return;
|
|
4323
4498
|
if (event.kind === 'opened') {
|
|
4324
4499
|
socketOpen = true;
|
|
4500
|
+
announced = false;
|
|
4501
|
+
handed = false;
|
|
4502
|
+
naming = 0;
|
|
4325
4503
|
connectionLive();
|
|
4326
4504
|
generation += 1;
|
|
4327
|
-
reads.opened();
|
|
4328
4505
|
conversations.reopened();
|
|
4329
4506
|
recovery.resume();
|
|
4330
|
-
|
|
4507
|
+
}
|
|
4508
|
+
else if (event.kind === 'discovered') {
|
|
4509
|
+
if (!socketOpen)
|
|
4510
|
+
return;
|
|
4511
|
+
announced = true;
|
|
4512
|
+
void sync('opened', event.discovery);
|
|
4331
4513
|
}
|
|
4332
4514
|
else if (event.kind === 'closed')
|
|
4333
4515
|
closedSocket(event.revoked, event.lost);
|
|
4334
|
-
else if (event.kind === '
|
|
4335
|
-
void
|
|
4516
|
+
else if (event.kind === 'caught-up')
|
|
4517
|
+
void caughtUpRooms(event.expired);
|
|
4336
4518
|
else if (event.kind === 'authority-changed')
|
|
4337
|
-
void
|
|
4519
|
+
void changeAuthority(event);
|
|
4338
4520
|
else
|
|
4339
|
-
void applyRoom(event
|
|
4340
|
-
}
|
|
4341
|
-
}
|
|
4342
|
-
async function heldRevisions(locator, paths) {
|
|
4343
|
-
const held = replicas.get(locator);
|
|
4344
|
-
if (held === undefined || paths.length === 0)
|
|
4345
|
-
return new Map();
|
|
4346
|
-
const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
|
|
4347
|
-
return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
|
|
4521
|
+
void applyRoom(event);
|
|
4522
|
+
});
|
|
4348
4523
|
}
|
|
4349
4524
|
function writesHeld() {
|
|
4350
4525
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -4414,8 +4589,9 @@ export function composeRuntime(options) {
|
|
|
4414
4589
|
if (closed)
|
|
4415
4590
|
return;
|
|
4416
4591
|
closed = true;
|
|
4417
|
-
|
|
4418
|
-
|
|
4592
|
+
notifyDiscovered();
|
|
4593
|
+
if (probing !== null)
|
|
4594
|
+
clearInterval(probing);
|
|
4419
4595
|
connectionLive();
|
|
4420
4596
|
lockListeners.clear();
|
|
4421
4597
|
readinessWatches.clear();
|
|
@@ -4427,6 +4603,7 @@ export function composeRuntime(options) {
|
|
|
4427
4603
|
conversations.close();
|
|
4428
4604
|
void recovery.retire();
|
|
4429
4605
|
roomListeners.clear();
|
|
4606
|
+
unlistedRooms.clear();
|
|
4430
4607
|
refusalListeners.clear();
|
|
4431
4608
|
storageLossListeners.clear();
|
|
4432
4609
|
statusListeners.clear();
|
|
@@ -4439,6 +4616,7 @@ export function composeRuntime(options) {
|
|
|
4439
4616
|
}
|
|
4440
4617
|
async function stop() {
|
|
4441
4618
|
stopping = true;
|
|
4619
|
+
inFlight.abort();
|
|
4442
4620
|
await recovery.retire();
|
|
4443
4621
|
await Promise.all(working);
|
|
4444
4622
|
await mutations;
|
|
@@ -4452,12 +4630,12 @@ export function composeRuntime(options) {
|
|
|
4452
4630
|
observeStream();
|
|
4453
4631
|
void openLocal().then(() => {
|
|
4454
4632
|
if (capabilities.length > 0)
|
|
4455
|
-
|
|
4633
|
+
readyFromStore();
|
|
4456
4634
|
}, () => {
|
|
4457
|
-
|
|
4635
|
+
readyFromStore();
|
|
4458
4636
|
});
|
|
4459
4637
|
if (options.adapter.roomStream === undefined)
|
|
4460
|
-
void sync('unstreamed');
|
|
4638
|
+
void sync('unstreamed', null);
|
|
4461
4639
|
function planned(input, reach) {
|
|
4462
4640
|
const plan = planOf(input, roomFacts(reach), planReach(reach));
|
|
4463
4641
|
const named = plan.paths !== null &&
|
|
@@ -4602,12 +4780,13 @@ export function composeRuntime(options) {
|
|
|
4602
4780
|
};
|
|
4603
4781
|
}
|
|
4604
4782
|
if (topic === 'readiness') {
|
|
4605
|
-
return watchReadiness({ target: '', code: 'stored',
|
|
4783
|
+
return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
|
|
4606
4784
|
}
|
|
4607
4785
|
if (topic === 'rooms') {
|
|
4608
|
-
|
|
4609
|
-
|
|
4786
|
+
unlistedRooms.add(deliver);
|
|
4787
|
+
emitKnownRooms();
|
|
4610
4788
|
return () => {
|
|
4789
|
+
unlistedRooms.delete(deliver);
|
|
4611
4790
|
roomListeners.delete(deliver);
|
|
4612
4791
|
};
|
|
4613
4792
|
}
|
|
@@ -4661,7 +4840,17 @@ export function composeRuntime(options) {
|
|
|
4661
4840
|
return openLocal().then(() => undefined);
|
|
4662
4841
|
},
|
|
4663
4842
|
rooms() {
|
|
4664
|
-
|
|
4843
|
+
if (closed)
|
|
4844
|
+
return Promise.resolve([]);
|
|
4845
|
+
return new Promise((settle) => {
|
|
4846
|
+
const answer = (value) => {
|
|
4847
|
+
unlistedRooms.delete(answer);
|
|
4848
|
+
roomListeners.delete(answer);
|
|
4849
|
+
settle(value);
|
|
4850
|
+
};
|
|
4851
|
+
unlistedRooms.add(answer);
|
|
4852
|
+
emitKnownRooms();
|
|
4853
|
+
});
|
|
4665
4854
|
},
|
|
4666
4855
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
4667
4856
|
search: (input) => searched(input, null, []),
|
|
@@ -4671,7 +4860,7 @@ export function composeRuntime(options) {
|
|
|
4671
4860
|
typedSelection: rooms.get(locator)?.typedSelection === undefined ||
|
|
4672
4861
|
rooms.get(locator)?.typedSelection === null
|
|
4673
4862
|
? null
|
|
4674
|
-
: modelTypes(
|
|
4863
|
+
: modelTypes(builtIn),
|
|
4675
4864
|
};
|
|
4676
4865
|
},
|
|
4677
4866
|
roomDraft: {
|
|
@@ -4688,33 +4877,17 @@ export function composeRuntime(options) {
|
|
|
4688
4877
|
const record = roomDrafts.get(room)?.get(path);
|
|
4689
4878
|
return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
|
|
4690
4879
|
},
|
|
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
|
-
});
|
|
4880
|
+
write(room, path, revision, input, next = roomDraftRevision()) {
|
|
4881
|
+
if (typeof next !== 'string' || next === '')
|
|
4882
|
+
return Promise.reject(runtimeError('room-draft-malformed'));
|
|
4883
|
+
return gathered(`room-draft\u0000${room}\u0000${path}`, { revision, input, next }, (earlier, later) => {
|
|
4884
|
+
if (later.revision !== earlier.next)
|
|
4885
|
+
return null;
|
|
4886
|
+
const parts = gatheredParts(earlier.input, later.input);
|
|
4887
|
+
return parts === null
|
|
4888
|
+
? null
|
|
4889
|
+
: { revision: earlier.revision, input: parts, next: later.next };
|
|
4890
|
+
}, (written) => writtenRoomDraft(room, path, written.revision, written.input, written.next));
|
|
4718
4891
|
},
|
|
4719
4892
|
discard(room, path, revision) {
|
|
4720
4893
|
return exclusive(async () => {
|
|
@@ -4741,13 +4914,13 @@ export function composeRuntime(options) {
|
|
|
4741
4914
|
});
|
|
4742
4915
|
},
|
|
4743
4916
|
write(app, value) {
|
|
4744
|
-
return
|
|
4917
|
+
return exclusive(() => keptPresentation(app, () => structuredClone(value)));
|
|
4745
4918
|
},
|
|
4746
4919
|
edit(app, members) {
|
|
4747
|
-
return
|
|
4748
|
-
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(
|
|
4749
|
-
...Object.entries(
|
|
4750
|
-
]));
|
|
4920
|
+
return gathered(`presentation\u0000${app}`, structuredClone(members), (earlier, later) => ({ ...earlier, ...later }), (edited) => keptPresentation(app, (held) => Object.fromEntries([
|
|
4921
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(edited, member)),
|
|
4922
|
+
...Object.entries(edited).filter(([, value]) => value !== null),
|
|
4923
|
+
])));
|
|
4751
4924
|
},
|
|
4752
4925
|
},
|
|
4753
4926
|
save(room, named, writtenBy) {
|
|
@@ -4766,7 +4939,8 @@ export function composeRuntime(options) {
|
|
|
4766
4939
|
part: readPart,
|
|
4767
4940
|
read: readBinaryPath,
|
|
4768
4941
|
link: linkBinaryPath,
|
|
4769
|
-
|
|
4942
|
+
show: showBinaryPath,
|
|
4943
|
+
attach: attachBinary,
|
|
4770
4944
|
},
|
|
4771
4945
|
async call(name, input) {
|
|
4772
4946
|
await ready;
|
|
@@ -4789,7 +4963,8 @@ export function composeRuntime(options) {
|
|
|
4789
4963
|
journal.record('retry');
|
|
4790
4964
|
unpreparable.clear();
|
|
4791
4965
|
queueReadiness();
|
|
4792
|
-
|
|
4966
|
+
if (full)
|
|
4967
|
+
await probed();
|
|
4793
4968
|
},
|
|
4794
4969
|
observe: observing(watch),
|
|
4795
4970
|
async reauthorize() {
|
|
@@ -4803,7 +4978,7 @@ export function composeRuntime(options) {
|
|
|
4803
4978
|
authorization = undefined;
|
|
4804
4979
|
discoveryPaused = false;
|
|
4805
4980
|
if (options.adapter.roomStream === undefined)
|
|
4806
|
-
await sync('unstreamed');
|
|
4981
|
+
await sync('unstreamed', null);
|
|
4807
4982
|
else
|
|
4808
4983
|
await task(OPENING_LANE, ensureAuthorization);
|
|
4809
4984
|
},
|
|
@@ -4811,7 +4986,6 @@ export function composeRuntime(options) {
|
|
|
4811
4986
|
const reachable = {
|
|
4812
4987
|
md,
|
|
4813
4988
|
watch,
|
|
4814
|
-
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
4815
4989
|
plan: planned,
|
|
4816
4990
|
query: (plan) => rowsQuery(plan),
|
|
4817
4991
|
watchPlan,
|
|
@@ -4821,12 +4995,12 @@ export function composeRuntime(options) {
|
|
|
4821
4995
|
lockedRooms,
|
|
4822
4996
|
watchLocks,
|
|
4823
4997
|
located,
|
|
4998
|
+
room: (locator) => listed().find((one) => one.locator === locator),
|
|
4824
4999
|
};
|
|
4825
5000
|
return {
|
|
4826
5001
|
md,
|
|
4827
5002
|
seed,
|
|
4828
5003
|
stop,
|
|
4829
|
-
lose: loseRooms,
|
|
4830
5004
|
watch,
|
|
4831
5005
|
persisted(answer) {
|
|
4832
5006
|
if (answer === persistence)
|
|
@@ -4835,6 +5009,10 @@ export function composeRuntime(options) {
|
|
|
4835
5009
|
journal.record(`persistence ${answer}`);
|
|
4836
5010
|
queueReadiness();
|
|
4837
5011
|
},
|
|
4838
|
-
within: (reach, port) =>
|
|
5012
|
+
within: (reach, port) => {
|
|
5013
|
+
if (full)
|
|
5014
|
+
void probed();
|
|
5015
|
+
return narrowed(reachable, reach, port);
|
|
5016
|
+
},
|
|
4839
5017
|
};
|
|
4840
5018
|
}
|