@agentshouse/mdruntime 0.1.0-c1ffd2bc60355 → 0.1.0-c20970f71a6a9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -9
- package/dist/adapter.d.ts +89 -53
- 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/bootstrap.d.ts +10 -13
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +100 -89
- package/dist/client.d.ts +8 -8
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +52 -6
- 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 +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +113 -45
- package/dist/connection.d.ts +8 -7
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1311 -1067
- package/dist/conversation.d.ts +15 -14
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +222 -140
- package/dist/durability.d.ts +0 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/entry.d.ts +1 -0
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +2 -1
- package/dist/file.d.ts +2 -11
- package/dist/file.d.ts.map +1 -1
- package/dist/file.js +6 -4
- package/dist/host.d.ts +3 -6
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +12 -19
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/media.d.ts +8 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +218 -0
- package/dist/memory.d.ts +10 -6
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +178 -90
- package/dist/messages.d.ts +2 -2
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +15 -16
- 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 -5
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +16 -2
- package/dist/paths.d.ts +1 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +4 -0
- package/dist/phrase.d.ts +3 -0
- package/dist/phrase.d.ts.map +1 -0
- package/dist/phrase.js +8 -0
- package/dist/query.d.ts +10 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +20 -1
- package/dist/reach.d.ts +10 -3
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +53 -29
- package/dist/react.d.ts +1 -0
- package/dist/react.d.ts.map +1 -1
- package/dist/readiness.d.ts +3 -17
- package/dist/readiness.d.ts.map +1 -1
- package/dist/readiness.js +4 -10
- 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 +7 -5
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +47 -11
- package/dist/replica.d.ts +3 -5
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +7 -7
- package/dist/room-draft.d.ts +18 -3
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +94 -47
- package/dist/rows.d.ts +17 -7
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +120 -87
- package/dist/save.d.ts +2 -0
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +11 -5
- package/dist/search.d.ts +12 -63
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +9 -52
- 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 +30 -37
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +61 -66
- package/dist/stored.d.ts +41 -24
- package/dist/stored.d.ts.map +1 -1
- package/dist/stored.js +196 -214
- package/dist/tables.d.ts +48 -49
- package/dist/tables.d.ts.map +1 -1
- package/dist/tables.js +23 -62
- 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 +9 -8
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +3 -2
- package/package.json +10 -17
- package/AGENTS.md +0 -317
- 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 -20
- package/dist/chat/markdown.d.ts.map +0 -1
- package/dist/chat/markdown.js +0 -229
- 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/fulltext.d.ts +0 -27
- package/dist/fulltext.d.ts.map +0 -1
- package/dist/fulltext.js +0 -368
- package/dist/reads.d.ts +0 -11
- package/dist/reads.d.ts.map +0 -1
- package/dist/reads.js +0 -100
- package/dist/words.d.ts +0 -19
- package/dist/words.d.ts.map +0 -1
- package/dist/words.js +0 -150
package/dist/connection.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { attachedModel,
|
|
1
|
+
import { parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
|
|
2
|
+
import { attachedModel, builtInModel, roomTypesOf } from './attached-model.js';
|
|
3
3
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
4
4
|
import { observing, whole } from './observation.js';
|
|
5
|
-
import {
|
|
5
|
+
import { bodied, identityOf, planOf, plain, queryKey, } from './query.js';
|
|
6
6
|
import { createLiveQueries } from './live.js';
|
|
7
7
|
import { seededResult } from './seed.js';
|
|
8
8
|
import { documentKey, roomOfKey } from './rows.js';
|
|
@@ -12,18 +12,18 @@ 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, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
|
|
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, gatheredParts, 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
28
|
import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
|
|
29
29
|
import { readinessOf, } from './readiness.js';
|
|
@@ -31,9 +31,9 @@ import { FencedMap } from './fence.js';
|
|
|
31
31
|
import { sameBinary } from './overlay.js';
|
|
32
32
|
import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
33
33
|
import { identityTime, roomDraftRevision } from './identity.js';
|
|
34
|
-
import { beneath, normalize } from './paths.js';
|
|
34
|
+
import { ancestors, beneath, normalize } from './paths.js';
|
|
35
35
|
import { openRuntimeStore, } from './store.js';
|
|
36
|
-
import {
|
|
36
|
+
import { validatedNeighbourhood } from './validation.js';
|
|
37
37
|
import { unconfirmed } from './transfer.js';
|
|
38
38
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
39
39
|
function within(root, path) {
|
|
@@ -54,46 +54,44 @@ const CONVERSATION_LANE = 'conversations';
|
|
|
54
54
|
const ROOM_READS = 256;
|
|
55
55
|
const ROOM_ATTEMPTS = 16;
|
|
56
56
|
const ERASE_BATCH = 512;
|
|
57
|
-
const
|
|
58
|
-
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;
|
|
59
63
|
const QUERY_BYTES = 1 << 20;
|
|
60
64
|
const STAGE_BYTES = 1 << 20;
|
|
61
65
|
const STAGE_FLOOR = 1 << 16;
|
|
62
66
|
// A batch aims at this much database work, so a Save or a query waits behind at most one short write.
|
|
63
67
|
const STAGE_AIM_MS = 50;
|
|
64
|
-
const
|
|
65
|
-
|
|
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.
|
|
66
72
|
const ARRIVAL_REPORT_MS = 250;
|
|
67
|
-
const INSTALL_STEPS = [
|
|
68
|
-
'writing',
|
|
69
|
-
'downloading',
|
|
70
|
-
'replaying',
|
|
71
|
-
'listing',
|
|
72
|
-
];
|
|
73
|
-
const CLEAN_CHANGES = 2_000;
|
|
74
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.
|
|
75
74
|
const LOCK_GRACE_MS = 120_000;
|
|
76
75
|
function operationOf(input) {
|
|
77
76
|
const operation = input?.operation_id;
|
|
78
77
|
return typeof operation === 'string' ? operation : null;
|
|
79
78
|
}
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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));
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
|
-
|
|
89
|
-
return `room ${locator}`;
|
|
90
|
-
}
|
|
88
|
+
const TEXT_READS = 4;
|
|
91
89
|
function pendingKey(locator, id) {
|
|
92
90
|
return `${locator}\u0000${id}`;
|
|
93
91
|
}
|
|
94
92
|
export function composeRuntime(options) {
|
|
95
93
|
const localSaves = options.adapter.localSaves === true;
|
|
96
|
-
const roomTypes = roomTypesOf(options.adapter.roomTypes);
|
|
94
|
+
const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
|
|
97
95
|
let authorization = options.authorization;
|
|
98
96
|
let opening;
|
|
99
97
|
const fences = new Map();
|
|
@@ -103,44 +101,51 @@ export function composeRuntime(options) {
|
|
|
103
101
|
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
104
102
|
let discoveries = 0;
|
|
105
103
|
const replicas = new FencedMap(isFenced);
|
|
104
|
+
const reopened = new Set();
|
|
105
|
+
const usableWhenHeld = options.usableWhenHeld ?? [];
|
|
106
106
|
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
107
107
|
const pendings = new Map();
|
|
108
108
|
const seeds = new Map();
|
|
109
109
|
const seedWatchers = new Map();
|
|
110
110
|
const roomDrafts = new FencedMap(isFenced);
|
|
111
|
+
const attachedFiles = new FencedMap(isFenced);
|
|
111
112
|
const presentations = new Map();
|
|
112
113
|
let limits = [];
|
|
113
114
|
let capabilities = [];
|
|
114
115
|
const rooms = new FencedMap(isFenced);
|
|
115
116
|
const counted = new Map();
|
|
116
117
|
const unreadable = new Set();
|
|
117
|
-
const
|
|
118
|
-
const models = new FencedMap(
|
|
119
|
-
const liveModels = new FencedMap(
|
|
118
|
+
const builtIn = builtInModel(roomTypes);
|
|
119
|
+
const models = new FencedMap(isFenced);
|
|
120
|
+
const liveModels = new FencedMap(isFenced);
|
|
120
121
|
const invalidModels = new Set();
|
|
121
122
|
const discovered = new Map();
|
|
122
|
-
|
|
123
|
+
let inFlight = new AbortController();
|
|
124
|
+
const refreshed = options.adapter.refresh?.bind(options.adapter);
|
|
125
|
+
const reads = {
|
|
126
|
+
bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
|
|
127
|
+
...(refreshed === undefined
|
|
128
|
+
? {}
|
|
129
|
+
: {
|
|
130
|
+
refresh: (authorized, request) => readUntil(refreshed(authorized, request), inFlight.signal),
|
|
131
|
+
}),
|
|
132
|
+
reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
|
|
133
|
+
};
|
|
123
134
|
let roomsListed = false;
|
|
124
135
|
const scopedRefusals = new Map();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
function modelKey(locator, attached) {
|
|
128
|
-
return `${attached} ${locator}`;
|
|
129
|
-
}
|
|
130
|
-
function modelOf(locator, attached = 'collection') {
|
|
131
|
-
return models.get(modelKey(locator, attached)) ?? NO_MODEL;
|
|
136
|
+
function modelOf(locator) {
|
|
137
|
+
return models.get(locator) ?? NO_MODEL;
|
|
132
138
|
}
|
|
133
|
-
function savedModel(locator
|
|
134
|
-
return invalidModels.has(
|
|
139
|
+
function savedModel(locator) {
|
|
140
|
+
return invalidModels.has(locator)
|
|
135
141
|
? { status: 'unavailable', reason: 'revoked' }
|
|
136
|
-
: modelOf(locator
|
|
142
|
+
: modelOf(locator);
|
|
143
|
+
}
|
|
144
|
+
function composed(record) {
|
|
145
|
+
return compositionOf(record, builtIn);
|
|
137
146
|
}
|
|
138
147
|
function compositionFor(record) {
|
|
139
|
-
return
|
|
140
|
-
...record,
|
|
141
|
-
model: savedModel(record.locator, 'collection'),
|
|
142
|
-
selectionModel: savedModel(record.locator, 'typed-selection'),
|
|
143
|
-
});
|
|
148
|
+
return composed({ ...record, model: savedModel(record.locator) });
|
|
144
149
|
}
|
|
145
150
|
let refusal = null;
|
|
146
151
|
let storageLoss = null;
|
|
@@ -150,10 +155,10 @@ export function composeRuntime(options) {
|
|
|
150
155
|
let closed = false;
|
|
151
156
|
let stopping = false;
|
|
152
157
|
let withdrawStream;
|
|
153
|
-
const announced = new Map();
|
|
154
158
|
let underway = null;
|
|
155
159
|
let streamed = null;
|
|
156
160
|
let handed = false;
|
|
161
|
+
let naming = 0;
|
|
157
162
|
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
158
163
|
const repairing = new Set();
|
|
159
164
|
const calling = new Set();
|
|
@@ -164,16 +169,18 @@ export function composeRuntime(options) {
|
|
|
164
169
|
let recorded = false;
|
|
165
170
|
const stale = new Set();
|
|
166
171
|
let generation = 0;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
const reloadsDue = new Map();
|
|
172
|
+
const reloadsDue = new Set();
|
|
173
|
+
const highest = new Map();
|
|
174
|
+
const beyondHorizon = new Set();
|
|
171
175
|
const lanes = new Map();
|
|
172
|
-
|
|
176
|
+
const roomLanes = new Map();
|
|
177
|
+
let roomWork = 0;
|
|
178
|
+
const roomReads = { free: ROOM_READS, waiting: [] };
|
|
173
179
|
const working = new Set();
|
|
174
180
|
let mutations = Promise.resolve();
|
|
175
181
|
let gathering = null;
|
|
176
182
|
const roomListeners = new Set();
|
|
183
|
+
const unlistedRooms = new Set();
|
|
177
184
|
const refusalListeners = new Set();
|
|
178
185
|
const storageLossListeners = new Set();
|
|
179
186
|
const statusListeners = new Set();
|
|
@@ -184,6 +191,8 @@ export function composeRuntime(options) {
|
|
|
184
191
|
let withdrawConversations;
|
|
185
192
|
let currencyQueued = null;
|
|
186
193
|
let socketOpen = false;
|
|
194
|
+
let announced = false;
|
|
195
|
+
let missedChange = false;
|
|
187
196
|
const confirmed = new Set();
|
|
188
197
|
let lockGrace = null;
|
|
189
198
|
const lockListeners = new Set();
|
|
@@ -193,6 +202,8 @@ export function composeRuntime(options) {
|
|
|
193
202
|
const serviceModeListeners = new Set();
|
|
194
203
|
let member = null;
|
|
195
204
|
const memberListeners = new Set();
|
|
205
|
+
let authorityChanges = 0;
|
|
206
|
+
const authorityChangeListeners = new Set();
|
|
196
207
|
const unpreparedDemand = new Set();
|
|
197
208
|
const unpreparable = new Set();
|
|
198
209
|
let preparing = false;
|
|
@@ -217,31 +228,32 @@ export function composeRuntime(options) {
|
|
|
217
228
|
let readinessQueued = false;
|
|
218
229
|
const catching = new Set();
|
|
219
230
|
const transfers = new FencedMap(isFenced);
|
|
220
|
-
const installs = new
|
|
231
|
+
const installs = new Set();
|
|
221
232
|
const arrivals = new Map();
|
|
233
|
+
const streaming = new Set();
|
|
234
|
+
const underived = new Map();
|
|
235
|
+
const unindexed = new Map();
|
|
236
|
+
const startedOver = new Map();
|
|
222
237
|
let arrivalReported = 0;
|
|
223
|
-
let
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
238
|
+
let streamsFree = STREAMS_HELD;
|
|
239
|
+
let queuedText = 0;
|
|
240
|
+
const installWaiters = [];
|
|
241
|
+
const stageBudget = {
|
|
242
|
+
bytes: STAGE_FLOOR * 4,
|
|
243
|
+
floor: STAGE_FLOOR,
|
|
244
|
+
ceiling: STAGE_BYTES,
|
|
245
|
+
aim: STAGE_AIM_MS,
|
|
246
|
+
};
|
|
247
|
+
const deriveBudget = {
|
|
248
|
+
bytes: DERIVE_FLOOR * 4,
|
|
249
|
+
floor: DERIVE_FLOOR,
|
|
250
|
+
ceiling: DERIVE_BYTES,
|
|
251
|
+
aim: DERIVE_AIM_MS,
|
|
252
|
+
};
|
|
253
|
+
const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
|
|
229
254
|
const conversations = createConversationStore({
|
|
230
|
-
agents: declaredAgents,
|
|
231
|
-
documents: (declaration, after) => rowsQuery(planOf({
|
|
232
|
-
rooms: [declaration.locator],
|
|
233
|
-
types: [declaration.type],
|
|
234
|
-
select: {
|
|
235
|
-
fields: [
|
|
236
|
-
declaration.identity,
|
|
237
|
-
declaration.name,
|
|
238
|
-
...(declaration.retired === undefined ? [] : [declaration.retired]),
|
|
239
|
-
].map((field) => `/${escapePointer(field)}`),
|
|
240
|
-
},
|
|
241
|
-
limit: MAXIMUM_WINDOW,
|
|
242
|
-
...(after === undefined ? {} : { after }),
|
|
243
|
-
}, roomFacts())),
|
|
244
255
|
call: (name, input) => invokeAction(name, input),
|
|
256
|
+
history: (request) => readConversationHistory(request),
|
|
245
257
|
durably,
|
|
246
258
|
stream: () => options.adapter.conversations,
|
|
247
259
|
publish: publishChangeSet,
|
|
@@ -253,10 +265,21 @@ export function composeRuntime(options) {
|
|
|
253
265
|
});
|
|
254
266
|
const journal = createLog();
|
|
255
267
|
const keptFiles = openedFiles(database, isFenced);
|
|
256
|
-
|
|
268
|
+
const holding = new Map();
|
|
269
|
+
const unholdable = new Map();
|
|
270
|
+
let readyFromStore = () => { };
|
|
257
271
|
const ready = new Promise((resolve) => {
|
|
258
|
-
|
|
272
|
+
readyFromStore = resolve;
|
|
273
|
+
});
|
|
274
|
+
let notifyDiscovered = () => { };
|
|
275
|
+
const firstDiscovery = new Promise((resolve) => {
|
|
276
|
+
notifyDiscovered = resolve;
|
|
259
277
|
});
|
|
278
|
+
let roomsNamed = false;
|
|
279
|
+
function notifyReady() {
|
|
280
|
+
notifyDiscovered();
|
|
281
|
+
readyFromStore();
|
|
282
|
+
}
|
|
260
283
|
function rememberRoomDraft(record) {
|
|
261
284
|
const room = roomDrafts.get(record.locator) ?? new Map();
|
|
262
285
|
room.set(record.path, record);
|
|
@@ -276,7 +299,7 @@ export function composeRuntime(options) {
|
|
|
276
299
|
return undefined;
|
|
277
300
|
const local = await openLocal();
|
|
278
301
|
const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
|
|
279
|
-
return { ...slice, collection: collectionOf(slice.documents,
|
|
302
|
+
return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
|
|
280
303
|
}
|
|
281
304
|
function roomDraftOf(record, visible) {
|
|
282
305
|
return structuredClone({
|
|
@@ -333,9 +356,9 @@ export function composeRuntime(options) {
|
|
|
333
356
|
return value === undefined ? null : structuredClone(value);
|
|
334
357
|
}
|
|
335
358
|
async function writtenRoomDraft(room, path, revision, input, next) {
|
|
336
|
-
const parts = readParts(input);
|
|
359
|
+
const parts = readParts(input, path);
|
|
337
360
|
if (path === '' || normalize(path) !== path)
|
|
338
|
-
throw runtimeError('room-draft-malformed');
|
|
361
|
+
throw runtimeError('room-draft-malformed', `room-draft-malformed at ${path}: a path inside the Room, such as notes/a.md`);
|
|
339
362
|
const local = await openLocal();
|
|
340
363
|
const current = roomDrafts.get(room)?.get(path);
|
|
341
364
|
if ((current?.revision ?? null) !== revision) {
|
|
@@ -343,7 +366,9 @@ export function composeRuntime(options) {
|
|
|
343
366
|
}
|
|
344
367
|
const file = parts.file === undefined
|
|
345
368
|
? undefined
|
|
346
|
-
: await heldFile(parts.file.blob,
|
|
369
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined
|
|
370
|
+
? binaryBase(await visibleOf(room, [path]), path)
|
|
371
|
+
: parts.file.base);
|
|
347
372
|
const record = {
|
|
348
373
|
locator: room,
|
|
349
374
|
path,
|
|
@@ -378,6 +403,20 @@ export function composeRuntime(options) {
|
|
|
378
403
|
(queuesWithoutReplica.size > 0 ||
|
|
379
404
|
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
380
405
|
}
|
|
406
|
+
function unsent() {
|
|
407
|
+
const changed = new Map();
|
|
408
|
+
for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
409
|
+
for (const { batch, state } of entriesOf(room)) {
|
|
410
|
+
if (state === 'local')
|
|
411
|
+
continue;
|
|
412
|
+
for (const { path } of batch.changes)
|
|
413
|
+
changed.set(`${room}\u0000${path}`, { room, path });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return [...changed]
|
|
417
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
418
|
+
.map(([, change]) => change);
|
|
419
|
+
}
|
|
381
420
|
function attention() {
|
|
382
421
|
const attentionRequired = [];
|
|
383
422
|
for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
|
|
@@ -389,16 +428,9 @@ export function composeRuntime(options) {
|
|
|
389
428
|
returned: structuredClone(record.returned),
|
|
390
429
|
});
|
|
391
430
|
}
|
|
392
|
-
for (const [room, refusal] of sourceRefusals)
|
|
393
|
-
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
394
431
|
attentionRequired.sort((left, right) => {
|
|
395
432
|
if (left.room !== right.room)
|
|
396
433
|
return left.room < right.room ? -1 : 1;
|
|
397
|
-
if ('refusal' in left || 'refusal' in right) {
|
|
398
|
-
if ('refusal' in left && 'refusal' in right)
|
|
399
|
-
return 0;
|
|
400
|
-
return 'refusal' in left ? -1 : 1;
|
|
401
|
-
}
|
|
402
434
|
if (left.path !== right.path)
|
|
403
435
|
return left.path < right.path ? -1 : 1;
|
|
404
436
|
return 0;
|
|
@@ -410,7 +442,7 @@ export function composeRuntime(options) {
|
|
|
410
442
|
if (reset === null || submission.reset < reset)
|
|
411
443
|
reset = submission.reset;
|
|
412
444
|
}
|
|
413
|
-
return { attentionRequired, reset };
|
|
445
|
+
return { attentionRequired, unsent: unsent(), reset };
|
|
414
446
|
}
|
|
415
447
|
function documentsIn(locator) {
|
|
416
448
|
return replicas.get(locator)?.count ?? 0;
|
|
@@ -483,7 +515,7 @@ export function composeRuntime(options) {
|
|
|
483
515
|
const governed = governs(reach, room);
|
|
484
516
|
const attached = [
|
|
485
517
|
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
486
|
-
...(governed.selection ? [
|
|
518
|
+
...(governed.selection ? [builtIn] : []),
|
|
487
519
|
];
|
|
488
520
|
return {
|
|
489
521
|
locator: room.locator,
|
|
@@ -499,11 +531,26 @@ export function composeRuntime(options) {
|
|
|
499
531
|
const value = listed();
|
|
500
532
|
for (const listener of roomListeners)
|
|
501
533
|
listener(value);
|
|
534
|
+
emitKnownRooms();
|
|
535
|
+
}
|
|
536
|
+
function emitKnownRooms() {
|
|
537
|
+
conversations.learned();
|
|
538
|
+
if (unlistedRooms.size === 0)
|
|
539
|
+
return;
|
|
540
|
+
if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
|
|
541
|
+
return;
|
|
542
|
+
const value = listed();
|
|
543
|
+
for (const listener of [...unlistedRooms]) {
|
|
544
|
+
unlistedRooms.delete(listener);
|
|
545
|
+
roomListeners.add(listener);
|
|
546
|
+
listener(value);
|
|
547
|
+
}
|
|
502
548
|
}
|
|
503
549
|
function emitRefusal() {
|
|
504
550
|
queueReadiness();
|
|
505
551
|
for (const listener of refusalListeners)
|
|
506
552
|
listener(refusal);
|
|
553
|
+
emitKnownRooms();
|
|
507
554
|
}
|
|
508
555
|
function emitStorageLoss() {
|
|
509
556
|
queueReadiness();
|
|
@@ -524,6 +571,7 @@ export function composeRuntime(options) {
|
|
|
524
571
|
if (closed)
|
|
525
572
|
return;
|
|
526
573
|
queueReadiness();
|
|
574
|
+
emitKnownRooms();
|
|
527
575
|
const next = status.state === 'synced' && !conversations.current()
|
|
528
576
|
? { ...status, state: 'syncing' }
|
|
529
577
|
: status;
|
|
@@ -537,9 +585,6 @@ export function composeRuntime(options) {
|
|
|
537
585
|
if (localSaves)
|
|
538
586
|
return Object.freeze({ batch, state: 'local' });
|
|
539
587
|
const state = pendings.get(pendingKey(locator, batch.id));
|
|
540
|
-
if (state?.accepted !== undefined) {
|
|
541
|
-
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
542
|
-
}
|
|
543
588
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
544
589
|
}
|
|
545
590
|
function entriesOf(locator) {
|
|
@@ -567,11 +612,14 @@ export function composeRuntime(options) {
|
|
|
567
612
|
if (held === undefined || closed || held.indexed)
|
|
568
613
|
return;
|
|
569
614
|
const key = held.record.key;
|
|
570
|
-
const done = await exclusive(async () =>
|
|
615
|
+
const done = await exclusive(async () => {
|
|
616
|
+
const local = await openLocal();
|
|
617
|
+
return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
|
|
618
|
+
});
|
|
571
619
|
const now = replicas.get(locator);
|
|
572
620
|
if (now?.record.key !== key)
|
|
573
621
|
continue;
|
|
574
|
-
if (!done)
|
|
622
|
+
if (!done.done)
|
|
575
623
|
continue;
|
|
576
624
|
replicas.set(locator, { ...now, indexed: true });
|
|
577
625
|
publishChangeSet(changeSet(locator, { documents: null }));
|
|
@@ -649,7 +697,7 @@ export function composeRuntime(options) {
|
|
|
649
697
|
coverage.push({
|
|
650
698
|
room: locator,
|
|
651
699
|
generation: replica.record.transfer,
|
|
652
|
-
current: !(catching.has(locator) ||
|
|
700
|
+
current: !(catching.has(locator) || transfers.has(locator)),
|
|
653
701
|
});
|
|
654
702
|
read.push({ locator, key: replica.record.key });
|
|
655
703
|
for (const batch of replica.queue)
|
|
@@ -676,8 +724,6 @@ export function composeRuntime(options) {
|
|
|
676
724
|
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
677
725
|
}
|
|
678
726
|
function roomState(locator) {
|
|
679
|
-
if (sourceRefusals.has(locator))
|
|
680
|
-
return 'refused';
|
|
681
727
|
const listedRoom = discovered.get(locator);
|
|
682
728
|
if (roomsListed &&
|
|
683
729
|
(listedRoom === undefined ||
|
|
@@ -692,7 +738,7 @@ export function composeRuntime(options) {
|
|
|
692
738
|
}
|
|
693
739
|
function conversationCoverage(reach) {
|
|
694
740
|
const declared = reach === null || reach.conversations;
|
|
695
|
-
if (!declared || options.adapter.conversations === undefined
|
|
741
|
+
if (!declared || options.adapter.conversations === undefined)
|
|
696
742
|
return 'undeclared';
|
|
697
743
|
if (!conversations.known() || !conversations.current())
|
|
698
744
|
return 'acquiring';
|
|
@@ -707,71 +753,50 @@ export function composeRuntime(options) {
|
|
|
707
753
|
? 'connected'
|
|
708
754
|
: 'connecting';
|
|
709
755
|
}
|
|
710
|
-
function
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
rule: held.code,
|
|
722
|
-
location: typeof location === 'string' ? location : null,
|
|
723
|
-
},
|
|
724
|
-
];
|
|
725
|
-
});
|
|
756
|
+
function bytesOf(room, underway) {
|
|
757
|
+
const transfer = transfers.get(room);
|
|
758
|
+
if (transfer !== undefined) {
|
|
759
|
+
const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
|
|
760
|
+
return {
|
|
761
|
+
done: transfer.done.bytes - Math.ceil(owed),
|
|
762
|
+
total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
|
|
766
|
+
return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
|
|
726
767
|
}
|
|
727
768
|
function installationOf(coverage) {
|
|
728
769
|
const rooms = { done: 0, total: 0 };
|
|
729
|
-
const parts = { done: 0, total: 0 };
|
|
730
770
|
const bytes = { done: 0, total: 0 };
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
let waiting = false;
|
|
771
|
+
let installing = 0;
|
|
772
|
+
let downloading = false;
|
|
734
773
|
for (const { room, state } of coverage) {
|
|
735
774
|
if (state === 'unadmitted')
|
|
736
775
|
continue;
|
|
737
776
|
rooms.total += 1;
|
|
738
777
|
if (state === 'complete')
|
|
739
778
|
rooms.done += 1;
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
if (
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
const installing = held !== undefined && state !== 'complete';
|
|
759
|
-
if (installing)
|
|
760
|
-
steps.add(held.step);
|
|
761
|
-
waiting ||= installing || state === 'acquiring';
|
|
762
|
-
}
|
|
763
|
-
return {
|
|
764
|
-
step: INSTALL_STEPS.find((step) => steps.has(step)) ?? null,
|
|
765
|
-
paused: waiting && !onScreen ? 'hidden' : null,
|
|
766
|
-
rooms,
|
|
767
|
-
parts,
|
|
768
|
-
bytes,
|
|
769
|
-
received,
|
|
770
|
-
};
|
|
779
|
+
const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
|
|
780
|
+
if (underway)
|
|
781
|
+
installing += 1;
|
|
782
|
+
if (underway && (streaming.has(room) || !arrivals.has(room)))
|
|
783
|
+
downloading = true;
|
|
784
|
+
const now = bytesOf(room, underway);
|
|
785
|
+
const earlier = underway ? startedOver.get(room) : undefined;
|
|
786
|
+
bytes.done += Math.max(now.done, earlier?.done ?? 0);
|
|
787
|
+
bytes.total += Math.max(now.total, earlier?.total ?? 0);
|
|
788
|
+
}
|
|
789
|
+
const step = installing === 0
|
|
790
|
+
? null
|
|
791
|
+
: downloading
|
|
792
|
+
? 'downloading'
|
|
793
|
+
: bytes.done < bytes.total
|
|
794
|
+
? 'writing'
|
|
795
|
+
: 'replaying';
|
|
796
|
+
return { step, rooms, bytes };
|
|
771
797
|
}
|
|
772
798
|
function readinessFor(port, reach) {
|
|
773
799
|
const required = requiredRooms(reach);
|
|
774
|
-
const search = port.search === 'required';
|
|
775
800
|
const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
|
|
776
801
|
const preparing = coverage
|
|
777
802
|
.filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
|
|
@@ -784,16 +809,15 @@ export function composeRuntime(options) {
|
|
|
784
809
|
listed: required.listed,
|
|
785
810
|
rooms: coverage,
|
|
786
811
|
conversations: conversationCoverage(reach),
|
|
787
|
-
search: search ? 'required' : 'unused',
|
|
788
812
|
},
|
|
789
813
|
persistence,
|
|
790
814
|
connection: connectionOf(),
|
|
791
815
|
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
792
816
|
space: full ? 'full' : port.space,
|
|
793
817
|
installation: installationOf(coverage),
|
|
794
|
-
refused: refusedIn(required.rooms),
|
|
795
818
|
lost: storageLoss !== null,
|
|
796
|
-
established: targets[port.target]?.established === true
|
|
819
|
+
established: targets[port.target]?.established === true ||
|
|
820
|
+
(usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
|
|
797
821
|
});
|
|
798
822
|
}
|
|
799
823
|
function establish(name) {
|
|
@@ -846,7 +870,8 @@ export function composeRuntime(options) {
|
|
|
846
870
|
probeEachMinute();
|
|
847
871
|
}
|
|
848
872
|
function installedTarget() {
|
|
849
|
-
return
|
|
873
|
+
return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
|
|
874
|
+
Object.values(targets).some((target) => target.established));
|
|
850
875
|
}
|
|
851
876
|
function probeEachMinute() {
|
|
852
877
|
if (!full || probing !== null || closed || !installedTarget())
|
|
@@ -877,8 +902,12 @@ export function composeRuntime(options) {
|
|
|
877
902
|
unpreparable.clear();
|
|
878
903
|
journal.record('space freed');
|
|
879
904
|
queueReadiness();
|
|
880
|
-
if (
|
|
881
|
-
|
|
905
|
+
if (closed || authorization === undefined)
|
|
906
|
+
return;
|
|
907
|
+
if (options.adapter.roomStream === undefined)
|
|
908
|
+
void sync('unstreamed', null);
|
|
909
|
+
else
|
|
910
|
+
void resynchronize();
|
|
882
911
|
}
|
|
883
912
|
function seedRoomHeld(room, version) {
|
|
884
913
|
return indexed(room) && reachedBy(room, version);
|
|
@@ -918,7 +947,24 @@ export function composeRuntime(options) {
|
|
|
918
947
|
function sameCover(left, right) {
|
|
919
948
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
920
949
|
}
|
|
950
|
+
async function unheldRows(local, plan, rows, rooms) {
|
|
951
|
+
if (plan.shape.kind !== 'documents' || !bodied(plan.shape))
|
|
952
|
+
return [];
|
|
953
|
+
if (!('documents' in rows) || !textOnline())
|
|
954
|
+
return [];
|
|
955
|
+
const named = new Map();
|
|
956
|
+
for (const { room, path, textHeld } of rows.documents) {
|
|
957
|
+
if (textHeld === false)
|
|
958
|
+
named.set(room, [...(named.get(room) ?? []), path]);
|
|
959
|
+
}
|
|
960
|
+
const unheld = [];
|
|
961
|
+
for (const { locator, key } of rooms) {
|
|
962
|
+
unheld.push(...(await unheldIn(local, locator, key, named.get(locator) ?? [])));
|
|
963
|
+
}
|
|
964
|
+
return unheld.filter((one) => unholdable.get(textKey(one)) !== one.revision);
|
|
965
|
+
}
|
|
921
966
|
async function coveredRows(plan, cover) {
|
|
967
|
+
const asked = new Set();
|
|
922
968
|
for (;;) {
|
|
923
969
|
const before = cover();
|
|
924
970
|
if ('pending' in before)
|
|
@@ -929,8 +975,14 @@ export function composeRuntime(options) {
|
|
|
929
975
|
const current = () => local.changes(keys) === changes;
|
|
930
976
|
const rows = await windowRows(local, plan, before.rooms, current);
|
|
931
977
|
const after = cover();
|
|
932
|
-
if (rows
|
|
978
|
+
if (rows === null || !sameCover(before, after))
|
|
979
|
+
continue;
|
|
980
|
+
const unheld = (await unheldRows(local, plan, rows, before.rooms)).filter((one) => !asked.has(textKey(one)));
|
|
981
|
+
if (unheld.length === 0)
|
|
933
982
|
return { rows, cover: before, current };
|
|
983
|
+
for (const one of unheld)
|
|
984
|
+
asked.add(textKey(one));
|
|
985
|
+
await holdTexts(unheld);
|
|
934
986
|
}
|
|
935
987
|
}
|
|
936
988
|
async function windowRows(local, plan, rooms, unchanged) {
|
|
@@ -1004,6 +1056,10 @@ export function composeRuntime(options) {
|
|
|
1004
1056
|
}
|
|
1005
1057
|
function publishChangeSet(set) {
|
|
1006
1058
|
applyChangeSet(set);
|
|
1059
|
+
for (const { locator, path } of set.documents) {
|
|
1060
|
+
if (path !== null)
|
|
1061
|
+
attachedFiles.get(locator)?.delete(path);
|
|
1062
|
+
}
|
|
1007
1063
|
if (set.rooms.length > 0 ||
|
|
1008
1064
|
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
1009
1065
|
emitRooms();
|
|
@@ -1022,6 +1078,8 @@ export function composeRuntime(options) {
|
|
|
1022
1078
|
}
|
|
1023
1079
|
async function refuse(reason, provider = null) {
|
|
1024
1080
|
discoveryPaused = true;
|
|
1081
|
+
inFlight.abort();
|
|
1082
|
+
inFlight = new AbortController();
|
|
1025
1083
|
try {
|
|
1026
1084
|
if (reason === 'revoked-authorization') {
|
|
1027
1085
|
await exclusive(async () => {
|
|
@@ -1029,7 +1087,7 @@ export function composeRuntime(options) {
|
|
|
1029
1087
|
const records = [...replicas].map(([locator, held]) => ({
|
|
1030
1088
|
locator,
|
|
1031
1089
|
held,
|
|
1032
|
-
record: { ...held.record, model: revoked
|
|
1090
|
+
record: { ...held.record, model: revoked },
|
|
1033
1091
|
}));
|
|
1034
1092
|
let answer = null;
|
|
1035
1093
|
try {
|
|
@@ -1039,18 +1097,14 @@ export function composeRuntime(options) {
|
|
|
1039
1097
|
locator,
|
|
1040
1098
|
key: record.key,
|
|
1041
1099
|
record: storedRecord(record),
|
|
1042
|
-
|
|
1100
|
+
composed: composedOf(composed(record)),
|
|
1043
1101
|
})),
|
|
1044
1102
|
});
|
|
1045
1103
|
}
|
|
1046
1104
|
catch {
|
|
1047
1105
|
journal.record('model revocation persistence failed');
|
|
1048
1106
|
}
|
|
1049
|
-
const keys = new Set(models.keys());
|
|
1050
|
-
for (const locator of rooms.keys()) {
|
|
1051
|
-
keys.add(modelKey(locator, 'collection'));
|
|
1052
|
-
keys.add(modelKey(locator, 'typed-selection'));
|
|
1053
|
-
}
|
|
1107
|
+
const keys = new Set([...models.keys(), ...rooms.keys()]);
|
|
1054
1108
|
for (const key of keys) {
|
|
1055
1109
|
invalidModels.add(key);
|
|
1056
1110
|
models.set(key, revoked);
|
|
@@ -1105,15 +1159,15 @@ export function composeRuntime(options) {
|
|
|
1105
1159
|
? null
|
|
1106
1160
|
: {
|
|
1107
1161
|
roots: selection.roots,
|
|
1108
|
-
model: modelTypes(
|
|
1162
|
+
model: modelTypes(builtIn),
|
|
1109
1163
|
},
|
|
1110
1164
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1111
1165
|
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
1112
1166
|
synchronizationAvailable,
|
|
1113
|
-
refusal: scopedRefusals.get(room.locator) ??
|
|
1167
|
+
refusal: scopedRefusals.get(room.locator) ?? null,
|
|
1114
1168
|
});
|
|
1115
1169
|
}
|
|
1116
|
-
async function applyDiscovery(found, started,
|
|
1170
|
+
async function applyDiscovery(found, started, left) {
|
|
1117
1171
|
const lockedBefore = lockedRooms();
|
|
1118
1172
|
for (const room of found) {
|
|
1119
1173
|
const fence = fences.get(room.locator);
|
|
@@ -1127,18 +1181,11 @@ export function composeRuntime(options) {
|
|
|
1127
1181
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
1128
1182
|
for (const locator of listedLocators)
|
|
1129
1183
|
scopedRefusals.delete(locator);
|
|
1130
|
-
if (
|
|
1184
|
+
if (left === undefined)
|
|
1131
1185
|
discovered.clear();
|
|
1132
|
-
else
|
|
1133
|
-
for (const locator of covered)
|
|
1134
|
-
discovered.delete(locator);
|
|
1135
1186
|
for (const room of listed)
|
|
1136
1187
|
discovered.set(room.locator, room);
|
|
1137
|
-
|
|
1138
|
-
if (options.adapter.exhaustiveDiscovery === true)
|
|
1139
|
-
await loseRooms(unlisted);
|
|
1140
|
-
else
|
|
1141
|
-
unlistedKept(unlisted);
|
|
1188
|
+
await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
|
|
1142
1189
|
for (const room of listed) {
|
|
1143
1190
|
const held = replicas.get(room.locator)?.record;
|
|
1144
1191
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -1169,15 +1216,6 @@ export function composeRuntime(options) {
|
|
|
1169
1216
|
]),
|
|
1170
1217
|
].filter((locator) => !listed.has(locator));
|
|
1171
1218
|
}
|
|
1172
|
-
function unlistedKept(unlisted) {
|
|
1173
|
-
for (const locator of unlisted) {
|
|
1174
|
-
const held = replicas.get(locator)?.record;
|
|
1175
|
-
if (held !== undefined)
|
|
1176
|
-
list(held, false);
|
|
1177
|
-
else
|
|
1178
|
-
rooms.delete(locator);
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
1219
|
function fence(locators) {
|
|
1182
1220
|
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1183
1221
|
const removed = fresh.map((locator) => ({
|
|
@@ -1191,26 +1229,24 @@ export function composeRuntime(options) {
|
|
|
1191
1229
|
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1192
1230
|
pendings.delete(pendingKey(locator, batch.id));
|
|
1193
1231
|
replicas.delete(locator);
|
|
1232
|
+
highest.delete(locator);
|
|
1233
|
+
beyondHorizon.delete(locator);
|
|
1194
1234
|
queuesWithoutReplica.delete(locator);
|
|
1195
1235
|
rooms.delete(locator);
|
|
1196
1236
|
roomDrafts.delete(locator);
|
|
1197
|
-
|
|
1237
|
+
attachedFiles.delete(locator);
|
|
1238
|
+
untransferred(locator);
|
|
1239
|
+
startedOver.delete(locator);
|
|
1198
1240
|
catching.delete(locator);
|
|
1199
1241
|
discovered.delete(locator);
|
|
1200
|
-
announced.delete(locator);
|
|
1201
1242
|
unreadable.delete(locator);
|
|
1202
1243
|
stale.delete(locator);
|
|
1203
1244
|
repairable.delete(locator);
|
|
1204
1245
|
unpreparable.delete(locator);
|
|
1205
1246
|
scopedRefusals.delete(locator);
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
const key = modelKey(locator, attached);
|
|
1210
|
-
models.delete(key);
|
|
1211
|
-
liveModels.delete(key);
|
|
1212
|
-
invalidModels.delete(key);
|
|
1213
|
-
}
|
|
1247
|
+
models.delete(locator);
|
|
1248
|
+
liveModels.delete(locator);
|
|
1249
|
+
invalidModels.delete(locator);
|
|
1214
1250
|
journal.record(`room lost ${locator}`);
|
|
1215
1251
|
}
|
|
1216
1252
|
if (fresh.length === 0)
|
|
@@ -1258,7 +1294,14 @@ export function composeRuntime(options) {
|
|
|
1258
1294
|
});
|
|
1259
1295
|
await eraseFenced(fresh);
|
|
1260
1296
|
}
|
|
1261
|
-
function
|
|
1297
|
+
function retyped(persisted) {
|
|
1298
|
+
if (persisted.status === 'unavailable')
|
|
1299
|
+
return persisted;
|
|
1300
|
+
const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
|
|
1301
|
+
return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
|
|
1302
|
+
}
|
|
1303
|
+
function restored(key, held) {
|
|
1304
|
+
const persisted = retyped(held);
|
|
1262
1305
|
const prior = models.get(key);
|
|
1263
1306
|
if (prior !== undefined &&
|
|
1264
1307
|
prior.status !== 'unavailable' &&
|
|
@@ -1289,8 +1332,9 @@ export function composeRuntime(options) {
|
|
|
1289
1332
|
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1290
1333
|
targets = held.targets;
|
|
1291
1334
|
transfers.clear();
|
|
1292
|
-
|
|
1293
|
-
|
|
1335
|
+
arrivals.clear();
|
|
1336
|
+
streaming.clear();
|
|
1337
|
+
startedOver.clear();
|
|
1294
1338
|
limits = held.providerLimits;
|
|
1295
1339
|
capabilities = held.capabilities;
|
|
1296
1340
|
member = held.member;
|
|
@@ -1300,7 +1344,7 @@ export function composeRuntime(options) {
|
|
|
1300
1344
|
presentations.clear();
|
|
1301
1345
|
for (const record of held.presentation)
|
|
1302
1346
|
presentations.set(record.app, record.value);
|
|
1303
|
-
conversations.hydrate(heldConversations, chatDrafts);
|
|
1347
|
+
conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
|
|
1304
1348
|
pendings.clear();
|
|
1305
1349
|
for (const queue of queues) {
|
|
1306
1350
|
for (const { batch, state } of queue.pending) {
|
|
@@ -1308,6 +1352,7 @@ export function composeRuntime(options) {
|
|
|
1308
1352
|
}
|
|
1309
1353
|
}
|
|
1310
1354
|
replicas.clear();
|
|
1355
|
+
reopened.clear();
|
|
1311
1356
|
queuesWithoutReplica.clear();
|
|
1312
1357
|
for (const [locator, queue] of queuedBatches) {
|
|
1313
1358
|
if (!held.rooms.has(locator))
|
|
@@ -1316,9 +1361,8 @@ export function composeRuntime(options) {
|
|
|
1316
1361
|
records.forEach((stored, index) => {
|
|
1317
1362
|
if (fences.has(stored.locator))
|
|
1318
1363
|
return;
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
const record = { ...stored, model, selectionModel };
|
|
1364
|
+
const record = { ...stored, model: restored(stored.locator, stored.model) };
|
|
1365
|
+
reopened.add(record.locator);
|
|
1322
1366
|
replicas.set(record.locator, {
|
|
1323
1367
|
record,
|
|
1324
1368
|
queue: queuedBatches.get(record.locator) ?? [],
|
|
@@ -1329,25 +1373,28 @@ export function composeRuntime(options) {
|
|
|
1329
1373
|
});
|
|
1330
1374
|
const existing = rooms.get(record.locator);
|
|
1331
1375
|
if (existing === undefined)
|
|
1332
|
-
list(record,
|
|
1376
|
+
list(record, record.roomVersion !== null);
|
|
1333
1377
|
else {
|
|
1334
1378
|
rooms.set(record.locator, {
|
|
1335
1379
|
...existing,
|
|
1336
1380
|
roomVersion: record.roomVersion,
|
|
1337
1381
|
operations: record.operations,
|
|
1338
1382
|
replica: record.replica,
|
|
1339
|
-
model: modelTypes(model),
|
|
1383
|
+
model: modelTypes(record.model),
|
|
1340
1384
|
typedSelection: record.typedSelection === null
|
|
1341
1385
|
? null
|
|
1342
|
-
: {
|
|
1343
|
-
roots: record.typedSelection.roots,
|
|
1344
|
-
model: modelTypes(selectionModel),
|
|
1345
|
-
},
|
|
1386
|
+
: { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
|
|
1346
1387
|
});
|
|
1347
1388
|
}
|
|
1348
1389
|
});
|
|
1349
|
-
|
|
1350
|
-
|
|
1390
|
+
try {
|
|
1391
|
+
for (const key of held.orphans)
|
|
1392
|
+
await opened.release(key);
|
|
1393
|
+
}
|
|
1394
|
+
catch (error) {
|
|
1395
|
+
if (!refusedForSpace(error))
|
|
1396
|
+
throw error;
|
|
1397
|
+
}
|
|
1351
1398
|
if (held.erasing || held.orphans.length > 0)
|
|
1352
1399
|
queueMicrotask(sweep);
|
|
1353
1400
|
return {
|
|
@@ -1449,6 +1496,16 @@ export function composeRuntime(options) {
|
|
|
1449
1496
|
}
|
|
1450
1497
|
const REWRITE_LINKS = ['field', 'body'];
|
|
1451
1498
|
const CHECKED_LINKS = ['field', 'section'];
|
|
1499
|
+
const BODY_LINKS = ['body'];
|
|
1500
|
+
async function textsNeeded(local, record, named) {
|
|
1501
|
+
const paths = [...new Set(named.map((one) => (typeof one === 'string' ? one : one.path)))];
|
|
1502
|
+
const unheld = await unheldIn(local, record.locator, record.key, paths);
|
|
1503
|
+
if (unheld.length === 0)
|
|
1504
|
+
return null;
|
|
1505
|
+
await holdTexts(unheld);
|
|
1506
|
+
const [first] = await unheldIn(local, record.locator, record.key, unheld.map(({ path }) => path));
|
|
1507
|
+
return first?.path ?? null;
|
|
1508
|
+
}
|
|
1452
1509
|
async function sliceAt(record, view, queue, extra = [], moved = []) {
|
|
1453
1510
|
const local = await openLocal();
|
|
1454
1511
|
const needed = new Set([...touchedBy(queue), ...extra]);
|
|
@@ -1529,7 +1586,7 @@ export function composeRuntime(options) {
|
|
|
1529
1586
|
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
|
|
1530
1587
|
}
|
|
1531
1588
|
function replayed(record, confirmedLayer, queue) {
|
|
1532
|
-
return stepped(replaying(confirmedLayer, queue, record.locator,
|
|
1589
|
+
return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
|
|
1533
1590
|
}
|
|
1534
1591
|
async function overlayDiff(record, before, visible, touched) {
|
|
1535
1592
|
const kept = before !== undefined && before.record.key === record.key
|
|
@@ -1549,8 +1606,6 @@ export function composeRuntime(options) {
|
|
|
1549
1606
|
function unpairedIn(locator, batch) {
|
|
1550
1607
|
if (!queueIn(locator).some((held) => held.id === batch.id))
|
|
1551
1608
|
return false;
|
|
1552
|
-
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
1553
|
-
return false;
|
|
1554
1609
|
return !leavingIn(batch, locator) || withdrawable(locator)(batch);
|
|
1555
1610
|
}
|
|
1556
1611
|
async function together(local, settlements) {
|
|
@@ -1592,7 +1647,7 @@ export function composeRuntime(options) {
|
|
|
1592
1647
|
const confirmedLayer = overrides === undefined
|
|
1593
1648
|
? read
|
|
1594
1649
|
: await stepped(slicingWith(read, overrides.set, overrides.removed));
|
|
1595
|
-
const composition =
|
|
1650
|
+
const composition = composed(record);
|
|
1596
1651
|
const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
|
|
1597
1652
|
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1598
1653
|
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
@@ -1635,6 +1690,7 @@ export function composeRuntime(options) {
|
|
|
1635
1690
|
key: record.key,
|
|
1636
1691
|
record: storedRecord(record),
|
|
1637
1692
|
roots: record.typedSelection?.roots ?? [],
|
|
1693
|
+
composed: composedOf(composition),
|
|
1638
1694
|
...settling.commit,
|
|
1639
1695
|
...(overrides === undefined
|
|
1640
1696
|
? {}
|
|
@@ -1698,69 +1754,64 @@ export function composeRuntime(options) {
|
|
|
1698
1754
|
};
|
|
1699
1755
|
}
|
|
1700
1756
|
function adoptModels(locator, record) {
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
if (model.status === 'current')
|
|
1709
|
-
liveModels.set(key, JSON.stringify(model.snapshot));
|
|
1710
|
-
else if (model.status === 'unavailable')
|
|
1711
|
-
liveModels.delete(key);
|
|
1712
|
-
}
|
|
1757
|
+
const model = record.model;
|
|
1758
|
+
models.set(locator, model);
|
|
1759
|
+
invalidModels.delete(locator);
|
|
1760
|
+
if (model.status === 'current')
|
|
1761
|
+
liveModels.set(locator, JSON.stringify(model.snapshot));
|
|
1762
|
+
else if (model.status === 'unavailable')
|
|
1763
|
+
liveModels.delete(locator);
|
|
1713
1764
|
}
|
|
1714
1765
|
function remodel(locator, answers, held) {
|
|
1715
|
-
const
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
};
|
|
1726
|
-
return {
|
|
1727
|
-
collection: taken(answers?.collection, held.collection, 'collection'),
|
|
1728
|
-
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
1729
|
-
};
|
|
1766
|
+
const answer = answers?.collection;
|
|
1767
|
+
if (answer === undefined)
|
|
1768
|
+
return held;
|
|
1769
|
+
const { state, refused } = attachedModel(answer, held.collection, roomTypes);
|
|
1770
|
+
if (refused !== null) {
|
|
1771
|
+
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
1772
|
+
journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
|
|
1773
|
+
}
|
|
1774
|
+
models.set(locator, state);
|
|
1775
|
+
return { collection: state };
|
|
1730
1776
|
}
|
|
1731
|
-
function publish(local, room,
|
|
1777
|
+
function publish(local, room, reached, landing, live) {
|
|
1732
1778
|
const landed = exclusive(async () => {
|
|
1733
1779
|
if (!live()) {
|
|
1734
1780
|
caughtUpTo(room.locator);
|
|
1735
1781
|
return true;
|
|
1736
1782
|
}
|
|
1737
1783
|
const prior = replicas.get(room.locator);
|
|
1738
|
-
if (landing.
|
|
1739
|
-
(
|
|
1740
|
-
|
|
1784
|
+
if (!stands(room.locator, landing.from)) {
|
|
1785
|
+
if (landing.kind === 'refreshed') {
|
|
1786
|
+
await local.unrefresh(reached.key);
|
|
1787
|
+
return false;
|
|
1788
|
+
}
|
|
1789
|
+
caughtUpTo(room.locator);
|
|
1790
|
+
await abandoned(local, room.locator);
|
|
1741
1791
|
return false;
|
|
1742
1792
|
}
|
|
1793
|
+
const record = landing.kind === 'refreshed' && prior !== undefined
|
|
1794
|
+
? {
|
|
1795
|
+
...prior.record,
|
|
1796
|
+
roomVersion: reached.roomVersion,
|
|
1797
|
+
...(landing.remodelled ? { model: reached.model } : {}),
|
|
1798
|
+
}
|
|
1799
|
+
: reached;
|
|
1743
1800
|
adoptModels(room.locator, record);
|
|
1744
1801
|
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
1745
|
-
const roomVersion = record.roomVersion;
|
|
1746
|
-
const accepted = queue.filter((batch) => {
|
|
1747
|
-
const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
|
|
1748
|
-
return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
|
|
1749
|
-
});
|
|
1750
1802
|
let set;
|
|
1751
1803
|
try {
|
|
1752
|
-
set = await settle(local, room.locator, prior, record, queue
|
|
1804
|
+
set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
|
|
1753
1805
|
? {
|
|
1754
1806
|
room: true,
|
|
1755
1807
|
view: 'refreshed',
|
|
1756
|
-
commit: { refreshed: true
|
|
1808
|
+
commit: { refreshed: true },
|
|
1757
1809
|
}
|
|
1758
1810
|
: {
|
|
1759
1811
|
room: true,
|
|
1760
1812
|
view: landing.refreshed ? 'refreshed' : 'confirmed',
|
|
1761
1813
|
commit: {
|
|
1762
1814
|
published: { from: prior?.record.key ?? null },
|
|
1763
|
-
composition: compositionOf(record),
|
|
1764
1815
|
...(landing.refreshed ? { refreshed: true } : {}),
|
|
1765
1816
|
},
|
|
1766
1817
|
});
|
|
@@ -1772,24 +1823,26 @@ export function composeRuntime(options) {
|
|
|
1772
1823
|
return true;
|
|
1773
1824
|
}
|
|
1774
1825
|
if (landing.kind === 'bootstrapped') {
|
|
1775
|
-
|
|
1826
|
+
highest.delete(room.locator);
|
|
1827
|
+
beyondHorizon.delete(room.locator);
|
|
1828
|
+
untransferred(room.locator);
|
|
1829
|
+
startedOver.delete(room.locator);
|
|
1776
1830
|
catching.delete(room.locator);
|
|
1777
|
-
|
|
1831
|
+
if (prior !== undefined && prior.record.key !== record.key)
|
|
1832
|
+
sweep();
|
|
1833
|
+
name(room.locator);
|
|
1778
1834
|
}
|
|
1779
|
-
for (const batch of accepted)
|
|
1780
|
-
pendings.delete(pendingKey(room.locator, batch.id));
|
|
1781
1835
|
stale.delete(room.locator);
|
|
1782
1836
|
unpreparable.delete(room.locator);
|
|
1783
1837
|
if (unreadable.delete(room.locator))
|
|
1784
1838
|
scopedRefusals.delete(room.locator);
|
|
1785
|
-
sourceRefusals.delete(room.locator);
|
|
1786
|
-
declarationRefusals.delete(room.locator);
|
|
1787
1839
|
list(room, true);
|
|
1788
1840
|
publishChangeSet(set);
|
|
1841
|
+
if (landing.kind === 'bootstrapped')
|
|
1842
|
+
void heldAgain(room.locator);
|
|
1789
1843
|
await unpair(local);
|
|
1790
1844
|
moveTo({ ...status, ...attention() });
|
|
1791
|
-
|
|
1792
|
-
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1845
|
+
if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1793
1846
|
submitLater(room.locator);
|
|
1794
1847
|
}
|
|
1795
1848
|
return true;
|
|
@@ -1801,7 +1854,7 @@ export function composeRuntime(options) {
|
|
|
1801
1854
|
const replica = replicas.get(locator);
|
|
1802
1855
|
const room = rooms.get(locator);
|
|
1803
1856
|
if (replica === undefined || room === undefined) {
|
|
1804
|
-
throw runtimeError('room
|
|
1857
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
1805
1858
|
}
|
|
1806
1859
|
return { local, replica, room };
|
|
1807
1860
|
}
|
|
@@ -1819,12 +1872,7 @@ export function composeRuntime(options) {
|
|
|
1819
1872
|
return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
|
|
1820
1873
|
}
|
|
1821
1874
|
async function saveParts(locator, named, writtenBy) {
|
|
1822
|
-
const read = named
|
|
1823
|
-
if (read.some(({ path }, at) => path === '' ||
|
|
1824
|
-
normalize(path) !== path ||
|
|
1825
|
-
read.findIndex((one) => one.path === path) !== at)) {
|
|
1826
|
-
throw runtimeError('room-draft-malformed');
|
|
1827
|
-
}
|
|
1875
|
+
const read = readSavedParts(named);
|
|
1828
1876
|
const target = await savedRoom(locator);
|
|
1829
1877
|
const saved = [];
|
|
1830
1878
|
const files = [];
|
|
@@ -1833,7 +1881,7 @@ export function composeRuntime(options) {
|
|
|
1833
1881
|
for (const { path, parts } of read) {
|
|
1834
1882
|
const file = parts.file === undefined
|
|
1835
1883
|
? undefined
|
|
1836
|
-
: await heldFile(parts.file.blob, binaryBase(visible, path));
|
|
1884
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
|
|
1837
1885
|
if (parts.file !== undefined && file !== undefined) {
|
|
1838
1886
|
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1839
1887
|
}
|
|
@@ -1868,13 +1916,27 @@ export function composeRuntime(options) {
|
|
|
1868
1916
|
const referring = removed.length === 0
|
|
1869
1917
|
? []
|
|
1870
1918
|
: (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
|
|
1871
|
-
const
|
|
1919
|
+
const unheld = await textsNeeded(local, record, [
|
|
1920
|
+
...paths,
|
|
1921
|
+
...referring,
|
|
1922
|
+
...(moved.length === 0
|
|
1923
|
+
? []
|
|
1924
|
+
: await local.referrers(record.key, 'confirmed', moved, REWRITE_LINKS)),
|
|
1925
|
+
...(removed.length === 0
|
|
1926
|
+
? []
|
|
1927
|
+
: await local.referrers(record.key, 'confirmed', removed, BODY_LINKS)),
|
|
1928
|
+
]);
|
|
1929
|
+
if (unheld !== null) {
|
|
1930
|
+
return { refusal: { code: 'transport-uncertainty', retryable: true, file: unheld } };
|
|
1931
|
+
}
|
|
1932
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
|
|
1872
1933
|
const { visible } = await replayed(record, confirmedLayer, replica.queue);
|
|
1873
|
-
const composition =
|
|
1934
|
+
const composition = composed(record);
|
|
1874
1935
|
return {
|
|
1875
1936
|
record,
|
|
1876
1937
|
confirmed: confirmedLayer,
|
|
1877
1938
|
visible,
|
|
1939
|
+
folders: new Set(await local.folders(record.key, 'visible', paths)),
|
|
1878
1940
|
*visibleOver(queue) {
|
|
1879
1941
|
return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
|
|
1880
1942
|
},
|
|
@@ -1886,13 +1948,7 @@ export function composeRuntime(options) {
|
|
|
1886
1948
|
return false;
|
|
1887
1949
|
const local = await openLocal();
|
|
1888
1950
|
const roots = record.typedSelection?.roots ?? [];
|
|
1889
|
-
|
|
1890
|
-
(await local.typedWithin(record.key, 'visible', roots, false))) {
|
|
1891
|
-
return true;
|
|
1892
|
-
}
|
|
1893
|
-
return (composition.selection !== null &&
|
|
1894
|
-
composition.selection.model.status === 'unavailable' &&
|
|
1895
|
-
(await local.typedWithin(record.key, 'visible', roots, true)));
|
|
1951
|
+
return local.typedWithin(record.key, 'visible', roots, false);
|
|
1896
1952
|
}
|
|
1897
1953
|
function textDiffers(left, right) {
|
|
1898
1954
|
return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
|
|
@@ -1903,18 +1959,6 @@ export function composeRuntime(options) {
|
|
|
1903
1959
|
return undefined;
|
|
1904
1960
|
return entry.binary !== null ? null : entry.content;
|
|
1905
1961
|
}
|
|
1906
|
-
function walkOf(local, key, view) {
|
|
1907
|
-
return async (after) => {
|
|
1908
|
-
const page = await local.walk(key, view, after, WALK_BYTES);
|
|
1909
|
-
return {
|
|
1910
|
-
entries: page.entries.map(({ path, content, binary }) => ({
|
|
1911
|
-
path,
|
|
1912
|
-
content: binary !== null ? null : content,
|
|
1913
|
-
})),
|
|
1914
|
-
next: page.next,
|
|
1915
|
-
};
|
|
1916
|
-
};
|
|
1917
|
-
}
|
|
1918
1962
|
function lookupOf(local, key, view) {
|
|
1919
1963
|
return async (paths) => {
|
|
1920
1964
|
if (paths.length === 0)
|
|
@@ -1931,37 +1975,6 @@ export function composeRuntime(options) {
|
|
|
1931
1975
|
const read = parseFrontmatter(entry.content ?? '');
|
|
1932
1976
|
return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
|
|
1933
1977
|
}
|
|
1934
|
-
function overriddenWalk(walk, overrides) {
|
|
1935
|
-
const sorted = [...overrides.keys()].sort();
|
|
1936
|
-
const beyond = (path) => {
|
|
1937
|
-
if (path === null)
|
|
1938
|
-
return 0;
|
|
1939
|
-
let low = 0;
|
|
1940
|
-
let high = sorted.length;
|
|
1941
|
-
while (low < high) {
|
|
1942
|
-
const middle = (low + high) >>> 1;
|
|
1943
|
-
if (sorted[middle] <= path)
|
|
1944
|
-
low = middle + 1;
|
|
1945
|
-
else
|
|
1946
|
-
high = middle;
|
|
1947
|
-
}
|
|
1948
|
-
return low;
|
|
1949
|
-
};
|
|
1950
|
-
return async (after) => {
|
|
1951
|
-
const page = await walk(after);
|
|
1952
|
-
const end = page.next;
|
|
1953
|
-
const added = sorted
|
|
1954
|
-
.slice(beyond(after), end === null ? sorted.length : beyond(end))
|
|
1955
|
-
.flatMap((path) => {
|
|
1956
|
-
const entry = overrides.get(path);
|
|
1957
|
-
return entry === null || entry === undefined
|
|
1958
|
-
? []
|
|
1959
|
-
: [{ path, content: entry.binary !== null ? null : entry.content }];
|
|
1960
|
-
});
|
|
1961
|
-
const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
1962
|
-
return { entries, next: end };
|
|
1963
|
-
};
|
|
1964
|
-
}
|
|
1965
1978
|
function overriddenLookup(lookup, overrides) {
|
|
1966
1979
|
return async (paths) => {
|
|
1967
1980
|
const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
|
|
@@ -1979,9 +1992,8 @@ export function composeRuntime(options) {
|
|
|
1979
1992
|
}
|
|
1980
1993
|
function validatorOf(record, confirmedLayer) {
|
|
1981
1994
|
return async (resulting, touched) => {
|
|
1982
|
-
const composition = compositionFor(record);
|
|
1983
1995
|
const local = await openLocal();
|
|
1984
|
-
const
|
|
1996
|
+
const changes = [];
|
|
1985
1997
|
const overrides = new Map();
|
|
1986
1998
|
const pace = paced();
|
|
1987
1999
|
for (const path of new Set([...confirmedLayer.held, ...touched])) {
|
|
@@ -1992,19 +2004,13 @@ export function composeRuntime(options) {
|
|
|
1992
2004
|
continue;
|
|
1993
2005
|
overrides.set(path, now);
|
|
1994
2006
|
if (textOf(was) !== textOf(now))
|
|
1995
|
-
|
|
2007
|
+
changes.push({ path, was: textOf(was), now: textOf(now) });
|
|
1996
2008
|
}
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
texts: textsOf(local, record.key, 'confirmed'),
|
|
2003
|
-
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2004
|
-
}));
|
|
2005
|
-
if (clean)
|
|
2006
|
-
return { verdict: 'accept' };
|
|
2007
|
-
return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
|
|
2009
|
+
return validatedNeighbourhood(changes, compositionFor(record), {
|
|
2010
|
+
lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
|
|
2011
|
+
texts: textsOf(local, record.key, 'confirmed'),
|
|
2012
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2013
|
+
});
|
|
2008
2014
|
};
|
|
2009
2015
|
}
|
|
2010
2016
|
function textsOf(local, key, view) {
|
|
@@ -2028,6 +2034,7 @@ export function composeRuntime(options) {
|
|
|
2028
2034
|
operations: room.operations,
|
|
2029
2035
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
2030
2036
|
visible: saving.visible,
|
|
2037
|
+
folders: saving.folders,
|
|
2031
2038
|
queued: queuedDocuments(replica.queue),
|
|
2032
2039
|
unmodeled: await unmodeledIn(record),
|
|
2033
2040
|
validate: validatorOf(record, saving.confirmed),
|
|
@@ -2048,6 +2055,7 @@ export function composeRuntime(options) {
|
|
|
2048
2055
|
locator,
|
|
2049
2056
|
key: saving.record.key,
|
|
2050
2057
|
roots: saving.record.typedSelection?.roots ?? [],
|
|
2058
|
+
composed: composedOf(composed(saving.record)),
|
|
2051
2059
|
overlay: diff.overlay,
|
|
2052
2060
|
});
|
|
2053
2061
|
}
|
|
@@ -2079,15 +2087,19 @@ export function composeRuntime(options) {
|
|
|
2079
2087
|
}
|
|
2080
2088
|
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
2081
2089
|
const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
|
|
2082
|
-
|
|
2090
|
+
if ('refusal' in sourceSaving)
|
|
2091
|
+
return { outcome: 'refused', ...sourceSaving.refusal };
|
|
2092
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
|
|
2083
2093
|
if (changed.length > 0)
|
|
2084
2094
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2085
2095
|
const target = replicas.get(destination);
|
|
2086
2096
|
const listedRoom = rooms.get(destination);
|
|
2087
2097
|
if (target === undefined || listedRoom === undefined) {
|
|
2088
|
-
throw runtimeError('room
|
|
2098
|
+
throw runtimeError('unknown-room', `Unknown Room ${destination}`);
|
|
2089
2099
|
}
|
|
2090
2100
|
const arriving = await savingIn(target, [], moves.map(({ to }) => to));
|
|
2101
|
+
if ('refusal' in arriving)
|
|
2102
|
+
return { outcome: 'refused', ...arriving.refusal };
|
|
2091
2103
|
const built = await buildMove({
|
|
2092
2104
|
source: await sideOf(locator, replica, room, sourceSaving),
|
|
2093
2105
|
destination: await sideOf(destination, target, listedRoom, arriving),
|
|
@@ -2148,10 +2160,10 @@ export function composeRuntime(options) {
|
|
|
2148
2160
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2149
2161
|
const { answer, touched } = committedBoth;
|
|
2150
2162
|
if (answer.fenced !== null) {
|
|
2151
|
-
throw runtimeError('room
|
|
2163
|
+
throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
|
|
2152
2164
|
}
|
|
2153
2165
|
if (fences.has(locator) || fences.has(destination)) {
|
|
2154
|
-
throw runtimeError('room
|
|
2166
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2155
2167
|
}
|
|
2156
2168
|
pendings.set(pendingKey(locator, id), {
|
|
2157
2169
|
drafts: saved,
|
|
@@ -2204,7 +2216,9 @@ export function composeRuntime(options) {
|
|
|
2204
2216
|
...saved.map(({ path }) => path),
|
|
2205
2217
|
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
2206
2218
|
]);
|
|
2207
|
-
|
|
2219
|
+
if ('refusal' in saving)
|
|
2220
|
+
return { outcome: 'refused', ...saving.refusal };
|
|
2221
|
+
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
|
|
2208
2222
|
if (changed.length > 0)
|
|
2209
2223
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2210
2224
|
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
|
|
@@ -2232,6 +2246,7 @@ export function composeRuntime(options) {
|
|
|
2232
2246
|
providerLimits: limits,
|
|
2233
2247
|
handle: room.handle,
|
|
2234
2248
|
visible: saving.visible,
|
|
2249
|
+
folders: saving.folders,
|
|
2235
2250
|
changes,
|
|
2236
2251
|
bases: parts.flatMap(({ bases }) => bases),
|
|
2237
2252
|
queued: queuedDocuments(replica.queue),
|
|
@@ -2321,10 +2336,10 @@ export function composeRuntime(options) {
|
|
|
2321
2336
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2322
2337
|
const { answer, touched } = done;
|
|
2323
2338
|
if (answer.fenced !== null) {
|
|
2324
|
-
throw runtimeError('room
|
|
2339
|
+
throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
|
|
2325
2340
|
}
|
|
2326
2341
|
if (fences.has(locator))
|
|
2327
|
-
throw runtimeError('room
|
|
2342
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2328
2343
|
for (const origin of withdrawn.keys())
|
|
2329
2344
|
pendings.delete(pendingKey(locator, origin));
|
|
2330
2345
|
for (const [id, drafts] of drafted) {
|
|
@@ -2373,7 +2388,7 @@ export function composeRuntime(options) {
|
|
|
2373
2388
|
throw new Error('connection refused');
|
|
2374
2389
|
await local.keepUpload(locator, identifier, upload);
|
|
2375
2390
|
if (fences.has(locator))
|
|
2376
|
-
throw runtimeError('room
|
|
2391
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2377
2392
|
const key = pendingKey(locator, identifier);
|
|
2378
2393
|
pendings.set(key, { ...pendings.get(key), upload });
|
|
2379
2394
|
if (!continuing())
|
|
@@ -2382,6 +2397,9 @@ export function composeRuntime(options) {
|
|
|
2382
2397
|
async function readHistory(locator, path, query = {}) {
|
|
2383
2398
|
if (closed || refusal !== null)
|
|
2384
2399
|
return UNCERTAIN;
|
|
2400
|
+
await firstDiscovery;
|
|
2401
|
+
if (!roomsNamed)
|
|
2402
|
+
return UNCERTAIN;
|
|
2385
2403
|
try {
|
|
2386
2404
|
await ensureAuthorization();
|
|
2387
2405
|
}
|
|
@@ -2474,6 +2492,9 @@ export function composeRuntime(options) {
|
|
|
2474
2492
|
async function transferBinary(request, transfer, answer) {
|
|
2475
2493
|
if (closed || refusal !== null)
|
|
2476
2494
|
return UNCERTAIN;
|
|
2495
|
+
await firstDiscovery;
|
|
2496
|
+
if (!roomsNamed)
|
|
2497
|
+
return UNCERTAIN;
|
|
2477
2498
|
try {
|
|
2478
2499
|
await ensureAuthorization();
|
|
2479
2500
|
}
|
|
@@ -2504,23 +2525,104 @@ export function composeRuntime(options) {
|
|
|
2504
2525
|
return UNCERTAIN;
|
|
2505
2526
|
}
|
|
2506
2527
|
}
|
|
2528
|
+
async function heldAgain(locator) {
|
|
2529
|
+
const replica = replicas.get(locator);
|
|
2530
|
+
if (replica === undefined || !textOnline())
|
|
2531
|
+
return;
|
|
2532
|
+
try {
|
|
2533
|
+
const wanted = await (await openLocal()).wanted(locator, replica.record.key);
|
|
2534
|
+
await holdTexts(wanted.map(({ path, revision }) => ({ locator, path, revision })));
|
|
2535
|
+
}
|
|
2536
|
+
catch {
|
|
2537
|
+
journal.record(`hold text ${locator} unwritten`);
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
function textOnline() {
|
|
2541
|
+
return !closed && refusal === null && connectionOf() === 'connected';
|
|
2542
|
+
}
|
|
2543
|
+
function textKey({ locator, path }) {
|
|
2544
|
+
return documentKey(locator, path);
|
|
2545
|
+
}
|
|
2546
|
+
async function readText({ locator, path, revision }) {
|
|
2547
|
+
const replica = replicas.get(locator);
|
|
2548
|
+
if (replica === undefined || !textOnline())
|
|
2549
|
+
return;
|
|
2550
|
+
try {
|
|
2551
|
+
await ensureAuthorization();
|
|
2552
|
+
}
|
|
2553
|
+
catch {
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
2557
|
+
return;
|
|
2558
|
+
const since = fencings;
|
|
2559
|
+
journal.record(`hold text ${locator} ${path}`);
|
|
2560
|
+
const read = await reads
|
|
2561
|
+
.bootstrap(authorization, { locator, selection: replica.record.replica.selection, path })
|
|
2562
|
+
.catch(() => null);
|
|
2563
|
+
journal.record(`outcome text ${read?.kind ?? 'transport-uncertainty'}`);
|
|
2564
|
+
if (read === null || read.kind !== 'bootstrap' || fencedSince(locator, since))
|
|
2565
|
+
return;
|
|
2566
|
+
const content = read.documents[path];
|
|
2567
|
+
const now = replicas.get(locator);
|
|
2568
|
+
if (content === undefined || read.revisions[path] !== revision || now === undefined) {
|
|
2569
|
+
unholdable.set(textKey({ locator, path, revision }), revision);
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
const held = await (await openLocal()).hold(locator, now.record.key, compositionFor(now.record), [{ path, revision, content }]);
|
|
2573
|
+
if (held.length === 0)
|
|
2574
|
+
unholdable.set(textKey({ locator, path, revision }), revision);
|
|
2575
|
+
else
|
|
2576
|
+
publishChangeSet(changeSet(locator, { documents: held }));
|
|
2577
|
+
}
|
|
2578
|
+
function holdText(unheld) {
|
|
2579
|
+
const key = textKey(unheld);
|
|
2580
|
+
const running = holding.get(key);
|
|
2581
|
+
if (running !== undefined)
|
|
2582
|
+
return running;
|
|
2583
|
+
const started = readText(unheld).finally(() => holding.delete(key));
|
|
2584
|
+
holding.set(key, started);
|
|
2585
|
+
return started;
|
|
2586
|
+
}
|
|
2587
|
+
async function holdTexts(wanted) {
|
|
2588
|
+
const queue = wanted.filter((one) => unholdable.get(textKey(one)) !== one.revision);
|
|
2589
|
+
await Promise.all(Array.from({ length: Math.min(TEXT_READS, queue.length) }, async () => {
|
|
2590
|
+
for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
|
|
2591
|
+
await holdText(next);
|
|
2592
|
+
}
|
|
2593
|
+
}));
|
|
2594
|
+
}
|
|
2595
|
+
async function unheldIn(local, locator, key, paths) {
|
|
2596
|
+
if (paths.length === 0)
|
|
2597
|
+
return [];
|
|
2598
|
+
return (await local.entries(key, 'confirmed', paths)).flatMap((entry) => entry.content === null &&
|
|
2599
|
+
entry.binary === null &&
|
|
2600
|
+
entry.outline !== null &&
|
|
2601
|
+
entry.revision !== null
|
|
2602
|
+
? [{ locator, path: entry.path, revision: entry.revision }]
|
|
2603
|
+
: []);
|
|
2604
|
+
}
|
|
2605
|
+
function fetchBinary(request, name) {
|
|
2606
|
+
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
2607
|
+
}
|
|
2507
2608
|
async function readBinary(request, name) {
|
|
2508
2609
|
if (closed)
|
|
2509
2610
|
return UNCERTAIN;
|
|
2510
2611
|
const held = await keptFiles.find(name);
|
|
2511
2612
|
if (held !== null)
|
|
2512
2613
|
return held;
|
|
2513
|
-
return
|
|
2614
|
+
return fetchBinary(request, name);
|
|
2514
2615
|
}
|
|
2515
2616
|
async function binaryPath(locator, path, revision) {
|
|
2516
2617
|
if (revision !== undefined)
|
|
2517
2618
|
return { kind: 'path', locator, path, revision };
|
|
2518
2619
|
const replica = replicas.get(locator);
|
|
2519
|
-
if (replica === undefined)
|
|
2520
|
-
return null;
|
|
2521
2620
|
const local = await openLocal();
|
|
2522
|
-
const [held] = await local.entries(replica.record.key, 'visible', [path]);
|
|
2523
|
-
|
|
2621
|
+
const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
|
|
2622
|
+
const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
|
|
2623
|
+
if (attached !== undefined)
|
|
2624
|
+
return { kind: 'path', locator, path, revision: attached.revision };
|
|
2625
|
+
if (replica === undefined || held?.binary === undefined || held.binary === null)
|
|
2524
2626
|
return null;
|
|
2525
2627
|
if (held.binary.state === 'pending')
|
|
2526
2628
|
return { kind: 'pending', localId: held.binary.localId };
|
|
@@ -2552,47 +2654,99 @@ export function composeRuntime(options) {
|
|
|
2552
2654
|
revision: record.revision,
|
|
2553
2655
|
};
|
|
2554
2656
|
}
|
|
2555
|
-
async function
|
|
2657
|
+
async function askedBinary(locator, path, revision) {
|
|
2556
2658
|
await ready;
|
|
2557
2659
|
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2558
|
-
|
|
2660
|
+
return drafted !== undefined && drafted.localId === revision
|
|
2559
2661
|
? { kind: 'pending', localId: revision }
|
|
2560
|
-
:
|
|
2662
|
+
: binaryPath(locator, path, revision);
|
|
2663
|
+
}
|
|
2664
|
+
function pendingBinary(locator, path, localId) {
|
|
2665
|
+
const facts = queuedFacts(locator, path, localId);
|
|
2666
|
+
return facts === null
|
|
2667
|
+
? absentBinary(path)
|
|
2668
|
+
: {
|
|
2669
|
+
kind: 'binary',
|
|
2670
|
+
file: localId,
|
|
2671
|
+
byteLength: facts.byteLength,
|
|
2672
|
+
mediaType: facts.mediaType,
|
|
2673
|
+
};
|
|
2674
|
+
}
|
|
2675
|
+
async function readBinaryPath(locator, path, revision) {
|
|
2676
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2561
2677
|
if (asked === null)
|
|
2562
2678
|
return absentBinary(path);
|
|
2563
|
-
if (asked.kind === 'pending')
|
|
2564
|
-
|
|
2565
|
-
return facts === null
|
|
2566
|
-
? absentBinary(path)
|
|
2567
|
-
: {
|
|
2568
|
-
kind: 'binary',
|
|
2569
|
-
file: asked.localId,
|
|
2570
|
-
byteLength: facts.byteLength,
|
|
2571
|
-
mediaType: facts.mediaType,
|
|
2572
|
-
};
|
|
2573
|
-
}
|
|
2679
|
+
if (asked.kind === 'pending')
|
|
2680
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2574
2681
|
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
2575
2682
|
}
|
|
2683
|
+
async function heldMediaType(locator, path) {
|
|
2684
|
+
const replica = replicas.get(locator);
|
|
2685
|
+
const [held] = replica === undefined
|
|
2686
|
+
? []
|
|
2687
|
+
: await (await openLocal()).entries(replica.record.key, 'visible', [path]);
|
|
2688
|
+
return held === undefined
|
|
2689
|
+
? attachedFiles.get(locator)?.get(path)?.mediaType
|
|
2690
|
+
: held.binary?.mediaType;
|
|
2691
|
+
}
|
|
2692
|
+
function linkOf(asked, held) {
|
|
2693
|
+
return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
|
|
2694
|
+
}
|
|
2576
2695
|
async function linkBinaryPath(locator, path, revision) {
|
|
2577
2696
|
await ready;
|
|
2578
2697
|
const asked = await binaryPath(locator, path, revision);
|
|
2579
2698
|
if (asked === null || asked.kind === 'pending')
|
|
2580
2699
|
return absentBinary(path);
|
|
2581
|
-
return
|
|
2700
|
+
return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
|
|
2701
|
+
}
|
|
2702
|
+
async function showBinaryPath(locator, path, revision) {
|
|
2703
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2704
|
+
if (asked === null)
|
|
2705
|
+
return absentBinary(path);
|
|
2706
|
+
if (asked.kind === 'pending')
|
|
2707
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2708
|
+
if (closed)
|
|
2709
|
+
return UNCERTAIN;
|
|
2710
|
+
const name = { locator, path: asked.path, revision: asked.revision };
|
|
2711
|
+
const kept = await keptFiles.find(name);
|
|
2712
|
+
if (kept !== null)
|
|
2713
|
+
return kept;
|
|
2714
|
+
const mediaType = await heldMediaType(locator, path);
|
|
2715
|
+
if (mediaType?.startsWith('image/') === true)
|
|
2716
|
+
return fetchBinary(asked, name);
|
|
2717
|
+
return linkOf(asked, revision === undefined ? mediaType : undefined);
|
|
2718
|
+
}
|
|
2719
|
+
async function attachBinary(locator, file) {
|
|
2720
|
+
await ready;
|
|
2721
|
+
await firstDiscovery;
|
|
2722
|
+
if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
|
|
2723
|
+
return OFFLINE;
|
|
2724
|
+
const request = await attachedFile(locator, file);
|
|
2725
|
+
return transferBinary({ locator, path: file.name }, async (held) => {
|
|
2726
|
+
const result = await options.adapter.attach(held, request);
|
|
2727
|
+
if (result.kind === 'attached') {
|
|
2728
|
+
const landed = attachedFiles.get(locator) ?? new Map();
|
|
2729
|
+
landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
|
|
2730
|
+
attachedFiles.set(locator, landed);
|
|
2731
|
+
}
|
|
2732
|
+
return result;
|
|
2733
|
+
}, attachAnswer);
|
|
2582
2734
|
}
|
|
2583
2735
|
function readPart(file, part) {
|
|
2584
2736
|
return database.ask({ kind: 'file.part', key: file, part });
|
|
2585
2737
|
}
|
|
2586
2738
|
async function cleanUp() {
|
|
2587
2739
|
const freed = await database.ask({ kind: 'opened.clean' });
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2740
|
+
const local = await openLocal();
|
|
2741
|
+
let texts = 0;
|
|
2742
|
+
for (const [locator, replica] of replicas) {
|
|
2743
|
+
const released = await local.unhold(locator, replica.record.key, compositionFor(replica.record));
|
|
2744
|
+
texts += released.bytes;
|
|
2745
|
+
if (released.paths.length > 0) {
|
|
2746
|
+
publishChangeSet(changeSet(locator, { documents: released.paths }));
|
|
2747
|
+
}
|
|
2594
2748
|
}
|
|
2595
|
-
return
|
|
2749
|
+
return freed + texts + ((await options.shell.cleanUp?.()) ?? 0);
|
|
2596
2750
|
}
|
|
2597
2751
|
function declaredAction(name) {
|
|
2598
2752
|
for (const capability of capabilities) {
|
|
@@ -2645,6 +2799,24 @@ export function composeRuntime(options) {
|
|
|
2645
2799
|
return UNCERTAIN;
|
|
2646
2800
|
}
|
|
2647
2801
|
}
|
|
2802
|
+
async function readConversationHistory(request) {
|
|
2803
|
+
await ready;
|
|
2804
|
+
const stream = options.adapter.conversations;
|
|
2805
|
+
if (closed || refusal !== null || stream?.history === undefined)
|
|
2806
|
+
return UNCERTAIN;
|
|
2807
|
+
journal.record('transfer conversation history');
|
|
2808
|
+
try {
|
|
2809
|
+
const result = await stream.history(request);
|
|
2810
|
+
if (result.kind === 'revoked-authorization')
|
|
2811
|
+
await refuse('revoked-authorization');
|
|
2812
|
+
journal.record(`outcome conversation history ${result.kind}`);
|
|
2813
|
+
return calledResult(result);
|
|
2814
|
+
}
|
|
2815
|
+
catch {
|
|
2816
|
+
journal.record('outcome conversation history transport-uncertainty');
|
|
2817
|
+
return UNCERTAIN;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2648
2820
|
function unsettled(result) {
|
|
2649
2821
|
return result.kind !== 'action' && result.retryable;
|
|
2650
2822
|
}
|
|
@@ -2706,6 +2878,33 @@ export function composeRuntime(options) {
|
|
|
2706
2878
|
const held = replicas.get(locator);
|
|
2707
2879
|
return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
|
|
2708
2880
|
}
|
|
2881
|
+
function shownAt(locator) {
|
|
2882
|
+
const held = replicas.get(locator)?.record.roomVersion;
|
|
2883
|
+
const raised = highest.get(locator);
|
|
2884
|
+
if (held === undefined || raised === undefined)
|
|
2885
|
+
return held ?? raised;
|
|
2886
|
+
return options.adapter.reached(locator, held, raised) ? held : raised;
|
|
2887
|
+
}
|
|
2888
|
+
function showing(locator, roomVersion) {
|
|
2889
|
+
const at = shownAt(locator);
|
|
2890
|
+
if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
|
|
2891
|
+
highest.set(locator, roomVersion);
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
function older(locator, roomVersion) {
|
|
2895
|
+
if (beyondHorizon.has(locator))
|
|
2896
|
+
return false;
|
|
2897
|
+
const at = shownAt(locator);
|
|
2898
|
+
return (at !== undefined &&
|
|
2899
|
+
options.adapter.reached(locator, at, roomVersion) &&
|
|
2900
|
+
!options.adapter.reached(locator, roomVersion, at));
|
|
2901
|
+
}
|
|
2902
|
+
function replaced(locators) {
|
|
2903
|
+
for (const locator of locators) {
|
|
2904
|
+
highest.delete(locator);
|
|
2905
|
+
beyondHorizon.add(locator);
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2709
2908
|
async function writtenAfter(locator, roomVersion, path) {
|
|
2710
2909
|
const held = replicas.get(locator);
|
|
2711
2910
|
if (held === undefined)
|
|
@@ -2716,122 +2915,34 @@ export function composeRuntime(options) {
|
|
|
2716
2915
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
2717
2916
|
!options.adapter.reached(locator, roomVersion, written));
|
|
2718
2917
|
}
|
|
2719
|
-
function
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
else
|
|
2729
|
-
known.set(path, entry.binary !== null ? null : entry.content);
|
|
2730
|
-
}
|
|
2731
|
-
return known;
|
|
2732
|
-
};
|
|
2733
|
-
}
|
|
2734
|
-
async function unreadableRoom(texts, walk, paths) {
|
|
2735
|
-
if (paths !== null) {
|
|
2736
|
-
const text = {};
|
|
2737
|
-
for (const [path, content] of await texts(paths))
|
|
2738
|
-
if (content !== null)
|
|
2739
|
-
text[path] = content;
|
|
2740
|
-
return stepped(unreadableIn(text, Object.keys(text)));
|
|
2741
|
-
}
|
|
2742
|
-
for (let after = null;;) {
|
|
2743
|
-
const page = await walk(after);
|
|
2744
|
-
const text = {};
|
|
2745
|
-
for (const { path, content } of page.entries)
|
|
2746
|
-
if (content !== null)
|
|
2747
|
-
text[path] = content;
|
|
2748
|
-
const refused = await stepped(unreadableIn(text, Object.keys(text)));
|
|
2749
|
-
if (refused !== null)
|
|
2750
|
-
return refused;
|
|
2751
|
-
if (page.next === null)
|
|
2752
|
-
return null;
|
|
2753
|
-
after = page.next;
|
|
2754
|
-
}
|
|
2755
|
-
}
|
|
2756
|
-
async function validatedRoom(local, record, view, previous, overrides = null) {
|
|
2757
|
-
const composition = compositionOf(record);
|
|
2758
|
-
const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
|
|
2759
|
-
const lookup = overrides === null
|
|
2760
|
-
? lookupOf(local, record.key, view)
|
|
2761
|
-
: overriddenLookup(lookupOf(local, record.key, view), overrides);
|
|
2762
|
-
const texts = overrides === null
|
|
2763
|
-
? textsOf(local, record.key, view)
|
|
2764
|
-
: overriddenTexts(textsOf(local, record.key, view), overrides);
|
|
2765
|
-
const walk = overrides === null
|
|
2766
|
-
? walkOf(local, record.key, view)
|
|
2767
|
-
: overriddenWalk(walkOf(local, record.key, view), overrides);
|
|
2768
|
-
let changes = null;
|
|
2769
|
-
let reshaped = false;
|
|
2770
|
-
if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
|
|
2771
|
-
const [was, now] = await Promise.all([
|
|
2772
|
-
local.entries(record.key, 'confirmed', previous.changed),
|
|
2773
|
-
overrides === null
|
|
2774
|
-
? local.entries(record.key, view, previous.changed)
|
|
2775
|
-
: Promise.resolve([]),
|
|
2776
|
-
]);
|
|
2777
|
-
const before = new Map(was.map((entry) => [entry.path, entry]));
|
|
2778
|
-
const after = overrides === null
|
|
2779
|
-
? new Map(now.map((entry) => [entry.path, entry]))
|
|
2780
|
-
: overrides;
|
|
2781
|
-
changes = previous.changed.flatMap((path) => {
|
|
2782
|
-
const one = before.get(path) ?? null;
|
|
2783
|
-
const other = after.get(path) ?? null;
|
|
2784
|
-
reshaped ||= textDiffers(one, other);
|
|
2785
|
-
const held = textOf(one);
|
|
2786
|
-
const next = textOf(other);
|
|
2787
|
-
return held === next ? [] : [{ path, was: held, now: next }];
|
|
2788
|
-
});
|
|
2789
|
-
}
|
|
2790
|
-
const named = changes === null ? null : changes.map(({ path }) => path);
|
|
2791
|
-
const textChanged = changes === null || changes.length > 0 || reshaped;
|
|
2792
|
-
const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
|
|
2793
|
-
if (modeled && (textChanged || modelsChanged)) {
|
|
2794
|
-
const clean = previous !== null &&
|
|
2795
|
-
changes !== null &&
|
|
2796
|
-
(await changedCleanly(changes, compositionOf(previous.record), composition, {
|
|
2797
|
-
lookup,
|
|
2798
|
-
texts,
|
|
2799
|
-
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2800
|
-
}));
|
|
2801
|
-
if (clean)
|
|
2802
|
-
return null;
|
|
2803
|
-
const verdict = await validatedCollection(walk, lookup, composition);
|
|
2804
|
-
if (verdict.verdict === 'accept')
|
|
2805
|
-
return null;
|
|
2806
|
-
if (verdict.rule !== 'model-unavailable')
|
|
2807
|
-
return { kind: 'refused', refusal: verdict };
|
|
2808
|
-
return unreadableRoom(texts, walk, named);
|
|
2809
|
-
}
|
|
2810
|
-
if (textChanged)
|
|
2811
|
-
return unreadableRoom(texts, walk, named);
|
|
2812
|
-
return null;
|
|
2918
|
+
async function writtenLater(local, locator, key, paths, roomVersion) {
|
|
2919
|
+
if (paths.length === 0 || !older(locator, roomVersion))
|
|
2920
|
+
return new Set();
|
|
2921
|
+
const entries = await local.entries(key, 'confirmed', paths);
|
|
2922
|
+
return new Set(entries.flatMap(({ path, written }) => written !== null &&
|
|
2923
|
+
options.adapter.reached(locator, written, roomVersion) &&
|
|
2924
|
+
!options.adapter.reached(locator, roomVersion, written)
|
|
2925
|
+
? [path]
|
|
2926
|
+
: []));
|
|
2813
2927
|
}
|
|
2814
|
-
function refreshingOf(local, key, liveness, replaying =
|
|
2815
|
-
const write = replaying
|
|
2928
|
+
function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
|
|
2929
|
+
const write = replaying ? finishingWrite : exclusive;
|
|
2816
2930
|
return {
|
|
2817
|
-
|
|
2818
|
-
if (replaying
|
|
2819
|
-
|
|
2820
|
-
installing(replaying, 'replaying');
|
|
2821
|
-
await screened();
|
|
2931
|
+
turn() {
|
|
2932
|
+
if (replaying)
|
|
2933
|
+
installing(locator);
|
|
2822
2934
|
},
|
|
2823
2935
|
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2936
|
+
later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion),
|
|
2824
2937
|
async stage(page) {
|
|
2825
2938
|
for (let written = 0;;) {
|
|
2826
|
-
if (replaying !== null)
|
|
2827
|
-
await screened();
|
|
2828
2939
|
const end = batchEnd(page.entries, written);
|
|
2829
2940
|
const last = end === page.entries.length;
|
|
2830
2941
|
const entries = page.entries.slice(written, end);
|
|
2831
2942
|
const staged = await write(async () => {
|
|
2832
2943
|
if (!liveness())
|
|
2833
2944
|
return false;
|
|
2834
|
-
await timed(() => local.refresh(key, last
|
|
2945
|
+
await timed(stageBudget, () => local.refresh(key, composition, last
|
|
2835
2946
|
? { ...page, entries }
|
|
2836
2947
|
: { entries, removals: [], folders: [], folderRemovals: [] }));
|
|
2837
2948
|
return true;
|
|
@@ -2850,94 +2961,180 @@ export function composeRuntime(options) {
|
|
|
2850
2961
|
let spent = 0;
|
|
2851
2962
|
while (end < entries.length) {
|
|
2852
2963
|
const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
|
|
2853
|
-
if (spent + size >
|
|
2964
|
+
if (spent + size > stageBudget.bytes && end > from)
|
|
2854
2965
|
break;
|
|
2855
2966
|
spent += size;
|
|
2856
2967
|
end += 1;
|
|
2857
2968
|
}
|
|
2858
2969
|
return end;
|
|
2859
2970
|
}
|
|
2860
|
-
async function timed(work) {
|
|
2971
|
+
async function timed(budget, work) {
|
|
2861
2972
|
const started = performance.now();
|
|
2862
2973
|
const done = await work();
|
|
2863
2974
|
const spent = performance.now() - started;
|
|
2864
|
-
if (spent <
|
|
2865
|
-
|
|
2866
|
-
else if (spent >
|
|
2867
|
-
|
|
2975
|
+
if (spent < budget.aim / 2)
|
|
2976
|
+
budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
|
|
2977
|
+
else if (spent > budget.aim * 2) {
|
|
2978
|
+
budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
|
|
2868
2979
|
}
|
|
2869
2980
|
return done;
|
|
2870
2981
|
}
|
|
2871
2982
|
function installWrite(work) {
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2983
|
+
return installTurn(installTurns.starting, work);
|
|
2984
|
+
}
|
|
2985
|
+
function finishingWrite(work) {
|
|
2986
|
+
return installTurn(installTurns.finishing, work);
|
|
2987
|
+
}
|
|
2988
|
+
function installTurn(queue, work) {
|
|
2989
|
+
return new Promise((resolve, reject) => {
|
|
2990
|
+
queue.push(() => void exclusive(work)
|
|
2991
|
+
.then(resolve, reject)
|
|
2992
|
+
.finally(() => {
|
|
2993
|
+
installTurns.busy = false;
|
|
2994
|
+
nextInstallTurn();
|
|
2995
|
+
}));
|
|
2996
|
+
nextInstallTurn();
|
|
2997
|
+
});
|
|
2875
2998
|
}
|
|
2876
|
-
function
|
|
2877
|
-
if (
|
|
2878
|
-
return
|
|
2879
|
-
|
|
2880
|
-
|
|
2999
|
+
function nextInstallTurn() {
|
|
3000
|
+
if (installTurns.busy || installTurns.ahead > 0)
|
|
3001
|
+
return;
|
|
3002
|
+
const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
|
|
3003
|
+
if (turn === undefined)
|
|
3004
|
+
return;
|
|
3005
|
+
installTurns.busy = true;
|
|
3006
|
+
turn();
|
|
3007
|
+
}
|
|
3008
|
+
async function ahead(work) {
|
|
3009
|
+
let taken = false;
|
|
3010
|
+
const hold = {
|
|
3011
|
+
take() {
|
|
3012
|
+
if (taken)
|
|
3013
|
+
return;
|
|
3014
|
+
taken = true;
|
|
3015
|
+
installTurns.ahead += 1;
|
|
3016
|
+
},
|
|
3017
|
+
release() {
|
|
3018
|
+
if (!taken)
|
|
3019
|
+
return;
|
|
3020
|
+
taken = false;
|
|
3021
|
+
installTurns.ahead -= 1;
|
|
3022
|
+
resumeInstall();
|
|
3023
|
+
},
|
|
3024
|
+
};
|
|
3025
|
+
hold.take();
|
|
3026
|
+
try {
|
|
3027
|
+
return await work(hold);
|
|
3028
|
+
}
|
|
3029
|
+
finally {
|
|
3030
|
+
hold.release();
|
|
3031
|
+
}
|
|
2881
3032
|
}
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
3033
|
+
function resumeInstall() {
|
|
3034
|
+
if (installTurns.ahead > 0)
|
|
3035
|
+
return;
|
|
3036
|
+
void yielded().then(nextInstallTurn);
|
|
3037
|
+
}
|
|
3038
|
+
async function heldStream() {
|
|
3039
|
+
while (streamsFree === 0)
|
|
3040
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
3041
|
+
streamsFree -= 1;
|
|
3042
|
+
return once(() => {
|
|
3043
|
+
streamsFree += 1;
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
3046
|
+
async function unqueued() {
|
|
3047
|
+
while (queuedText >= QUEUED_TEXT) {
|
|
3048
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
3049
|
+
}
|
|
2887
3050
|
}
|
|
2888
|
-
function
|
|
3051
|
+
function once(release) {
|
|
2889
3052
|
let released = false;
|
|
2890
3053
|
return () => {
|
|
2891
3054
|
if (released)
|
|
2892
3055
|
return;
|
|
2893
3056
|
released = true;
|
|
2894
|
-
|
|
2895
|
-
|
|
3057
|
+
release();
|
|
3058
|
+
for (const admit of installWaiters.splice(0))
|
|
3059
|
+
admit();
|
|
2896
3060
|
};
|
|
2897
3061
|
}
|
|
2898
|
-
function installing(locator
|
|
2899
|
-
installs.
|
|
3062
|
+
function installing(locator) {
|
|
3063
|
+
installs.add(locator);
|
|
2900
3064
|
queueReadiness();
|
|
2901
3065
|
}
|
|
2902
|
-
function
|
|
2903
|
-
|
|
2904
|
-
|
|
3066
|
+
function untransferred(locator) {
|
|
3067
|
+
transfers.delete(locator);
|
|
3068
|
+
arrivals.delete(locator);
|
|
3069
|
+
streaming.delete(locator);
|
|
3070
|
+
underived.delete(locator);
|
|
3071
|
+
unindexed.delete(locator);
|
|
3072
|
+
}
|
|
3073
|
+
function setBack(locator) {
|
|
3074
|
+
const now = bytesOf(locator, true);
|
|
3075
|
+
const earlier = startedOver.get(locator);
|
|
3076
|
+
startedOver.set(locator, {
|
|
3077
|
+
done: Math.max(now.done, earlier?.done ?? 0),
|
|
3078
|
+
total: Math.max(now.total, earlier?.total ?? 0),
|
|
3079
|
+
});
|
|
2905
3080
|
}
|
|
2906
|
-
function
|
|
2907
|
-
if (
|
|
3081
|
+
function owedIndexes(locator, bytes, text) {
|
|
3082
|
+
if (bytes <= 0 || text <= 0)
|
|
2908
3083
|
return;
|
|
2909
|
-
|
|
2910
|
-
|
|
3084
|
+
unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
|
|
3085
|
+
}
|
|
3086
|
+
function built(locator, { done, text }) {
|
|
3087
|
+
const owed = unindexed.get(locator) ?? [];
|
|
3088
|
+
let left = text;
|
|
3089
|
+
while (!done && left > 0 && owed.length > 0) {
|
|
3090
|
+
const [first] = owed;
|
|
3091
|
+
if (left >= first.text) {
|
|
3092
|
+
left -= first.text;
|
|
3093
|
+
owed.shift();
|
|
3094
|
+
continue;
|
|
3095
|
+
}
|
|
3096
|
+
owed[0] = {
|
|
3097
|
+
bytes: (first.bytes * (first.text - left)) / first.text,
|
|
3098
|
+
text: first.text - left,
|
|
3099
|
+
};
|
|
3100
|
+
left = 0;
|
|
3101
|
+
}
|
|
3102
|
+
if (done || owed.length === 0)
|
|
3103
|
+
unindexed.delete(locator);
|
|
2911
3104
|
queueReadiness();
|
|
2912
|
-
if (!value)
|
|
2913
|
-
return;
|
|
2914
|
-
for (const resolve of shownWaiters.splice(0))
|
|
2915
|
-
resolve();
|
|
2916
3105
|
}
|
|
2917
|
-
|
|
3106
|
+
function installed(locator) {
|
|
3107
|
+
if (installs.delete(locator))
|
|
3108
|
+
queueReadiness();
|
|
3109
|
+
}
|
|
3110
|
+
async function pullRefresh(authorized, held, liveness, hold, refresh) {
|
|
2918
3111
|
if (!liveness())
|
|
2919
3112
|
return unconfirmed;
|
|
2920
|
-
const turn = generation;
|
|
2921
3113
|
const { record } = held;
|
|
2922
|
-
const
|
|
3114
|
+
const from = standingOf(record.locator);
|
|
2923
3115
|
const local = await openLocal();
|
|
2924
3116
|
try {
|
|
2925
3117
|
await exclusive(() => local.unrefresh(record.key));
|
|
2926
|
-
const result = await advancement(
|
|
3118
|
+
const result = await advancement(async (authorization, request) => {
|
|
3119
|
+
hold.release();
|
|
3120
|
+
try {
|
|
3121
|
+
return await refresh(authorization, request);
|
|
3122
|
+
}
|
|
3123
|
+
finally {
|
|
3124
|
+
hold.take();
|
|
3125
|
+
}
|
|
3126
|
+
}, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness));
|
|
2927
3127
|
if (result.kind !== 'advanced') {
|
|
2928
3128
|
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2929
3129
|
return result;
|
|
2930
3130
|
}
|
|
2931
|
-
const refused = await validatedRoom(local, result.record, 'refreshed', {
|
|
2932
|
-
record,
|
|
2933
|
-
changed: [...result.changed],
|
|
2934
|
-
});
|
|
2935
|
-
if (refused !== null) {
|
|
2936
|
-
journal.record(`transfer refresh ${record.locator} refused`);
|
|
2937
|
-
return refused;
|
|
2938
|
-
}
|
|
2939
3131
|
journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
|
|
2940
|
-
return {
|
|
3132
|
+
return {
|
|
3133
|
+
kind: 'advanced',
|
|
3134
|
+
record: result.record,
|
|
3135
|
+
from,
|
|
3136
|
+
remodelled: result.record.model !== record.model,
|
|
3137
|
+
};
|
|
2941
3138
|
}
|
|
2942
3139
|
catch (error) {
|
|
2943
3140
|
if (refusedForSpace(error))
|
|
@@ -2945,9 +3142,6 @@ export function composeRuntime(options) {
|
|
|
2945
3142
|
journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
|
|
2946
3143
|
throw error;
|
|
2947
3144
|
}
|
|
2948
|
-
finally {
|
|
2949
|
-
reads.finished(record.locator, turn);
|
|
2950
|
-
}
|
|
2951
3145
|
}
|
|
2952
3146
|
const heldContents = new Map();
|
|
2953
3147
|
function heldContentOf(local, key) {
|
|
@@ -2965,33 +3159,50 @@ export function composeRuntime(options) {
|
|
|
2965
3159
|
heldContents.set(key, made);
|
|
2966
3160
|
return made;
|
|
2967
3161
|
}
|
|
3162
|
+
async function abandoned(local, locator) {
|
|
3163
|
+
await local.abandon(locator);
|
|
3164
|
+
setBack(locator);
|
|
3165
|
+
untransferred(locator);
|
|
3166
|
+
liveQueries.recover();
|
|
3167
|
+
sweep();
|
|
3168
|
+
}
|
|
2968
3169
|
function staging(local, liveness) {
|
|
3170
|
+
const derived = async (locator, key, composition) => {
|
|
3171
|
+
underived.delete(locator);
|
|
3172
|
+
for (;;) {
|
|
3173
|
+
if (!liveness())
|
|
3174
|
+
return false;
|
|
3175
|
+
const done = await finishingWrite(async () => liveness()
|
|
3176
|
+
? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
|
|
3177
|
+
: null);
|
|
3178
|
+
if (done === null)
|
|
3179
|
+
return false;
|
|
3180
|
+
built(locator, done);
|
|
3181
|
+
if (done.done)
|
|
3182
|
+
return true;
|
|
3183
|
+
}
|
|
3184
|
+
};
|
|
2969
3185
|
return {
|
|
2970
3186
|
held: (locator) => transfers.get(locator) ?? null,
|
|
2971
3187
|
async turn(locator, step) {
|
|
2972
|
-
installing(locator
|
|
2973
|
-
|
|
2974
|
-
await screened();
|
|
2975
|
-
const release = step === 'downloading' ? await heldPart() : () => { };
|
|
2976
|
-
if (onScreen || closed)
|
|
2977
|
-
return release;
|
|
2978
|
-
release();
|
|
2979
|
-
}
|
|
3188
|
+
installing(locator);
|
|
3189
|
+
return step === 'downloading' ? heldStream() : () => { };
|
|
2980
3190
|
},
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
3191
|
+
unqueued,
|
|
3192
|
+
queued(text) {
|
|
3193
|
+
queuedText += text;
|
|
3194
|
+
return once(() => {
|
|
3195
|
+
queuedText -= text;
|
|
3196
|
+
});
|
|
2986
3197
|
},
|
|
2987
3198
|
arriving(locator) {
|
|
2988
3199
|
let ended = false;
|
|
2989
3200
|
return {
|
|
2990
|
-
at(
|
|
3201
|
+
at(bytes) {
|
|
2991
3202
|
if (ended)
|
|
2992
3203
|
return;
|
|
2993
|
-
|
|
2994
|
-
|
|
3204
|
+
arrivals.set(locator, bytes);
|
|
3205
|
+
streaming.add(locator);
|
|
2995
3206
|
const now = performance.now();
|
|
2996
3207
|
if (now - arrivalReported < ARRIVAL_REPORT_MS)
|
|
2997
3208
|
return;
|
|
@@ -2999,96 +3210,64 @@ export function composeRuntime(options) {
|
|
|
2999
3210
|
queueReadiness();
|
|
3000
3211
|
},
|
|
3001
3212
|
end() {
|
|
3213
|
+
if (ended)
|
|
3214
|
+
return;
|
|
3002
3215
|
ended = true;
|
|
3003
|
-
if (
|
|
3216
|
+
if (streaming.delete(locator))
|
|
3004
3217
|
queueReadiness();
|
|
3005
3218
|
},
|
|
3006
3219
|
};
|
|
3007
3220
|
},
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
if (liveness())
|
|
3011
|
-
await local.keepPart(locator, token, received);
|
|
3012
|
-
}),
|
|
3013
|
-
announced(locator) {
|
|
3014
|
-
const later = announced.get(locator);
|
|
3015
|
-
return later === undefined ? [] : [later];
|
|
3016
|
-
},
|
|
3017
|
-
async stage(record, page, composition, received) {
|
|
3221
|
+
streamed: options.adapter.roomStream !== undefined,
|
|
3222
|
+
async stage(record, page, composition) {
|
|
3018
3223
|
const { locator } = record;
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
const
|
|
3023
|
-
let held = record;
|
|
3024
|
-
if (token !== null)
|
|
3025
|
-
arrivals.get(locator)?.delete(token);
|
|
3026
|
-
installing(locator, 'writing', { written: from, entries, bytes: whole });
|
|
3027
|
-
for (let written = from;;) {
|
|
3028
|
-
await screened();
|
|
3224
|
+
installing(locator);
|
|
3225
|
+
if (!liveness())
|
|
3226
|
+
return null;
|
|
3227
|
+
const key = await installWrite(async () => {
|
|
3029
3228
|
if (!liveness())
|
|
3030
3229
|
return null;
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
});
|
|
3051
|
-
if (key === null)
|
|
3052
|
-
return null;
|
|
3053
|
-
const arrived = !transfers.has(locator);
|
|
3054
|
-
held = { ...next, key };
|
|
3055
|
-
transfers.set(locator, held);
|
|
3056
|
-
if (arrived)
|
|
3057
|
-
liveQueries.recover();
|
|
3058
|
-
if (last) {
|
|
3059
|
-
installing(locator, held.pageToken === null ? 'replaying' : 'downloading');
|
|
3060
|
-
liveQueries.recover();
|
|
3061
|
-
return held;
|
|
3062
|
-
}
|
|
3063
|
-
installing(locator, 'writing', { written: end, entries, bytes: whole });
|
|
3064
|
-
written = end;
|
|
3065
|
-
}
|
|
3230
|
+
return local.stage(record, composition, page);
|
|
3231
|
+
});
|
|
3232
|
+
if (key === null || !liveness())
|
|
3233
|
+
return null;
|
|
3234
|
+
const arrived = !transfers.has(locator);
|
|
3235
|
+
const prior = transfers.get(locator)?.done.bytes ?? 0;
|
|
3236
|
+
const held = { ...record, key };
|
|
3237
|
+
transfers.set(locator, held);
|
|
3238
|
+
if (arrived)
|
|
3239
|
+
liveQueries.recover();
|
|
3240
|
+
installing(locator);
|
|
3241
|
+
liveQueries.recover();
|
|
3242
|
+
const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
|
|
3243
|
+
owedIndexes(locator, record.done.bytes - prior, text);
|
|
3244
|
+
const owed = text + (underived.get(locator) ?? 0);
|
|
3245
|
+
underived.set(locator, owed);
|
|
3246
|
+
if (owed < deriveBudget.bytes)
|
|
3247
|
+
return held;
|
|
3248
|
+
return (await derived(locator, key, composition)) ? held : null;
|
|
3066
3249
|
},
|
|
3067
|
-
|
|
3250
|
+
derive: derived,
|
|
3251
|
+
async refreshing(locator, key, composition) {
|
|
3068
3252
|
await exclusive(() => local.unrefresh(key));
|
|
3069
|
-
return refreshingOf(local, key, liveness,
|
|
3253
|
+
return refreshingOf(local, locator, key, composition, liveness, true);
|
|
3070
3254
|
},
|
|
3071
3255
|
abandon: (locator) => exclusive(async () => {
|
|
3072
|
-
if (
|
|
3073
|
-
|
|
3074
|
-
await local.abandon(locator);
|
|
3075
|
-
transfers.delete(locator);
|
|
3076
|
-
liveQueries.recover();
|
|
3077
|
-
sweep();
|
|
3256
|
+
if (liveness())
|
|
3257
|
+
await abandoned(local, locator);
|
|
3078
3258
|
}),
|
|
3079
|
-
setAside:
|
|
3259
|
+
setAside: async (locator) => {
|
|
3080
3260
|
if (!liveness())
|
|
3081
3261
|
return;
|
|
3082
|
-
|
|
3083
|
-
|
|
3262
|
+
setBack(locator);
|
|
3263
|
+
untransferred(locator);
|
|
3084
3264
|
liveQueries.recover();
|
|
3085
|
-
}
|
|
3265
|
+
},
|
|
3086
3266
|
release: (key) => exclusive(async () => {
|
|
3087
3267
|
await local.release(key);
|
|
3088
3268
|
sweep();
|
|
3089
3269
|
}),
|
|
3090
3270
|
content: (key) => heldContentOf(local, key),
|
|
3091
|
-
validate: (record, view) => validatedRoom(local, record, view, null),
|
|
3092
3271
|
};
|
|
3093
3272
|
}
|
|
3094
3273
|
function reusable(room, local) {
|
|
@@ -3100,18 +3279,21 @@ export function composeRuntime(options) {
|
|
|
3100
3279
|
async function pullBootstrap(authorized, room, local, liveness) {
|
|
3101
3280
|
if (!liveness())
|
|
3102
3281
|
return unconfirmed;
|
|
3103
|
-
const turn = generation;
|
|
3104
3282
|
if (replicas.has(room.locator)) {
|
|
3105
3283
|
catching.add(room.locator);
|
|
3106
3284
|
liveQueries.recover();
|
|
3107
3285
|
}
|
|
3108
3286
|
try {
|
|
3109
|
-
const result = await bootstrapRoom(reads, authorized, room, {
|
|
3110
|
-
collection: modelOf(room.locator),
|
|
3111
|
-
selection: modelOf(room.locator, 'typed-selection'),
|
|
3112
|
-
}, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
|
|
3287
|
+
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));
|
|
3113
3288
|
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
3114
3289
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
3290
|
+
if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
|
|
3291
|
+
caughtUpTo(room.locator);
|
|
3292
|
+
installed(room.locator);
|
|
3293
|
+
await abandoned(local, room.locator);
|
|
3294
|
+
journal.record(`transfer bootstrap ${room.locator} older than shown`);
|
|
3295
|
+
return { kind: 'transport-uncertainty' };
|
|
3296
|
+
}
|
|
3115
3297
|
if (result.kind !== 'staged') {
|
|
3116
3298
|
caughtUpTo(room.locator);
|
|
3117
3299
|
installed(room.locator);
|
|
@@ -3126,9 +3308,6 @@ export function composeRuntime(options) {
|
|
|
3126
3308
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
3127
3309
|
throw error;
|
|
3128
3310
|
}
|
|
3129
|
-
finally {
|
|
3130
|
-
reads.finished(room.locator, turn);
|
|
3131
|
-
}
|
|
3132
3311
|
}
|
|
3133
3312
|
function caughtUpTo(locator) {
|
|
3134
3313
|
if (catching.delete(locator))
|
|
@@ -3136,72 +3315,83 @@ export function composeRuntime(options) {
|
|
|
3136
3315
|
}
|
|
3137
3316
|
function behind(room) {
|
|
3138
3317
|
const held = replicas.get(room.locator)?.record.roomVersion;
|
|
3139
|
-
return
|
|
3140
|
-
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
3318
|
+
return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
|
|
3141
3319
|
}
|
|
3142
|
-
|
|
3143
|
-
const
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
return
|
|
3320
|
+
function standingOf(locator) {
|
|
3321
|
+
const held = replicas.get(locator);
|
|
3322
|
+
return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
|
|
3323
|
+
}
|
|
3324
|
+
function stands(locator, from) {
|
|
3325
|
+
const now = standingOf(locator);
|
|
3326
|
+
return now.key === from.key && now.confirmed === from.confirmed;
|
|
3149
3327
|
}
|
|
3150
|
-
async function
|
|
3328
|
+
async function advance(room, local, authorized, running) {
|
|
3151
3329
|
const live = () => running() && !full;
|
|
3152
3330
|
if (!live())
|
|
3153
3331
|
return unconfirmed;
|
|
3154
3332
|
const held = replicas.get(room.locator);
|
|
3155
|
-
if (held !== undefined) {
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
emitRooms();
|
|
3167
|
-
}
|
|
3168
|
-
return null;
|
|
3169
|
-
}
|
|
3170
|
-
if (refreshed.kind === 'advanced') {
|
|
3171
|
-
const landing = { kind: 'refreshed', base: refreshed.base };
|
|
3172
|
-
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3173
|
-
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3174
|
-
const reached = now !== undefined &&
|
|
3175
|
-
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3176
|
-
return reached ? null : advance(room, local, authorized, live);
|
|
3177
|
-
}
|
|
3178
|
-
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
3179
|
-
return null;
|
|
3180
|
-
}
|
|
3181
|
-
if (refreshed.kind !== 'beyond-horizon')
|
|
3333
|
+
if (held !== undefined && options.adapter.roomStream !== undefined) {
|
|
3334
|
+
stale.delete(room.locator);
|
|
3335
|
+
name(room.locator);
|
|
3336
|
+
return null;
|
|
3337
|
+
}
|
|
3338
|
+
const refresh = reads.refresh;
|
|
3339
|
+
if (held !== undefined && refresh !== undefined) {
|
|
3340
|
+
const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
|
|
3341
|
+
if (refreshed === 'behind')
|
|
3342
|
+
return advance(room, local, authorized, live);
|
|
3343
|
+
if (refreshed !== 'beyond-horizon')
|
|
3182
3344
|
return refreshed;
|
|
3183
|
-
if (
|
|
3184
|
-
owed.add(room.locator);
|
|
3185
|
-
liveQueries.recover();
|
|
3186
|
-
return unconfirmed;
|
|
3187
|
-
}
|
|
3188
|
-
if (reloadsDue.get(room.locator) === generation)
|
|
3345
|
+
if (reloadsDue.has(room.locator))
|
|
3189
3346
|
return unconfirmed;
|
|
3347
|
+
replaced([room.locator]);
|
|
3190
3348
|
}
|
|
3349
|
+
const from = standingOf(room.locator);
|
|
3191
3350
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
3192
|
-
if (bootstrapped.kind
|
|
3193
|
-
|
|
3351
|
+
if (bootstrapped.kind !== 'staged')
|
|
3352
|
+
return live() ? bootstrapped : unconfirmed;
|
|
3353
|
+
const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
|
|
3354
|
+
if (await publish(local, room, bootstrapped.record, landing, live)) {
|
|
3194
3355
|
return live() ? null : unconfirmed;
|
|
3195
3356
|
}
|
|
3196
|
-
return
|
|
3357
|
+
return reachedBy(room.locator, bootstrapped.record.roomVersion)
|
|
3358
|
+
? null
|
|
3359
|
+
: advance(room, local, authorized, live);
|
|
3360
|
+
}
|
|
3361
|
+
async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
|
|
3362
|
+
const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
|
|
3363
|
+
if (!live())
|
|
3364
|
+
return unconfirmed;
|
|
3365
|
+
if (refreshed.kind === 'still-current') {
|
|
3366
|
+
stale.delete(room.locator);
|
|
3367
|
+
if (unreadable.delete(room.locator)) {
|
|
3368
|
+
scopedRefusals.delete(room.locator);
|
|
3369
|
+
list(room, true);
|
|
3370
|
+
emitRooms();
|
|
3371
|
+
}
|
|
3372
|
+
return null;
|
|
3373
|
+
}
|
|
3374
|
+
if (refreshed.kind === 'advanced') {
|
|
3375
|
+
const landing = {
|
|
3376
|
+
kind: 'refreshed',
|
|
3377
|
+
from: refreshed.from,
|
|
3378
|
+
remodelled: refreshed.remodelled,
|
|
3379
|
+
};
|
|
3380
|
+
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3381
|
+
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3382
|
+
const reached = now !== undefined &&
|
|
3383
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3384
|
+
return reached ? null : 'behind';
|
|
3385
|
+
}
|
|
3386
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3387
|
+
return null;
|
|
3388
|
+
}
|
|
3389
|
+
return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
|
|
3197
3390
|
}
|
|
3198
3391
|
async function withhold(withheld, room, local, progress) {
|
|
3199
|
-
if (withheld.kind === 'unconfirmed')
|
|
3392
|
+
if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
|
|
3200
3393
|
return;
|
|
3201
|
-
|
|
3202
|
-
stale.delete(room.locator);
|
|
3203
|
-
else
|
|
3204
|
-
stale.add(room.locator);
|
|
3394
|
+
stale.add(room.locator);
|
|
3205
3395
|
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
3206
3396
|
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
3207
3397
|
repairable.set(room.locator, reset);
|
|
@@ -3223,30 +3413,10 @@ export function composeRuntime(options) {
|
|
|
3223
3413
|
list(room, false);
|
|
3224
3414
|
emitRooms();
|
|
3225
3415
|
}
|
|
3226
|
-
else if (withheld.kind === 'refused') {
|
|
3227
|
-
refusedSource(room, withheld);
|
|
3228
|
-
}
|
|
3229
3416
|
else if (withheld.kind === 'revoked-authorization') {
|
|
3230
3417
|
await refuse('revoked-authorization');
|
|
3231
3418
|
}
|
|
3232
3419
|
}
|
|
3233
|
-
function refusedSource(room, { refusal }) {
|
|
3234
|
-
sourceRefusals.set(room.locator, {
|
|
3235
|
-
code: refusal.rule,
|
|
3236
|
-
retryable: false,
|
|
3237
|
-
fields: {
|
|
3238
|
-
file: refusal.file,
|
|
3239
|
-
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
3240
|
-
...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
|
|
3241
|
-
},
|
|
3242
|
-
});
|
|
3243
|
-
declarationRefusals.delete(room.locator);
|
|
3244
|
-
unreadable.add(room.locator);
|
|
3245
|
-
list(room, false);
|
|
3246
|
-
emitRooms();
|
|
3247
|
-
moveTo({ ...status, ...attention() });
|
|
3248
|
-
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
3249
|
-
}
|
|
3250
3420
|
function leaving(batch, result) {
|
|
3251
3421
|
if (result.kind === 'expired')
|
|
3252
3422
|
return { batch, returned: returnedBy('expired') };
|
|
@@ -3268,8 +3438,8 @@ export function composeRuntime(options) {
|
|
|
3268
3438
|
function settling(acceptance) {
|
|
3269
3439
|
return acceptance !== null && 'apply' in acceptance;
|
|
3270
3440
|
}
|
|
3271
|
-
function
|
|
3272
|
-
return settling(acceptance) ?
|
|
3441
|
+
function failureOf(acceptance) {
|
|
3442
|
+
return settling(acceptance) ? null : acceptance;
|
|
3273
3443
|
}
|
|
3274
3444
|
async function acceptance(room, local, submitted, accepted) {
|
|
3275
3445
|
if (!continuing())
|
|
@@ -3282,7 +3452,8 @@ export function composeRuntime(options) {
|
|
|
3282
3452
|
logAccepted(room.locator, submitted, accepted);
|
|
3283
3453
|
let record = current.record;
|
|
3284
3454
|
let changes;
|
|
3285
|
-
if (!options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
3455
|
+
if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
|
|
3456
|
+
!older(room.locator, accepted.roomVersion)) {
|
|
3286
3457
|
const paths = [...returnedPaths(submitted, accepted)];
|
|
3287
3458
|
const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
|
|
3288
3459
|
const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
|
|
@@ -3292,34 +3463,6 @@ export function composeRuntime(options) {
|
|
|
3292
3463
|
? accepted.roomVersion
|
|
3293
3464
|
: held,
|
|
3294
3465
|
};
|
|
3295
|
-
const overrides = new Map([
|
|
3296
|
-
...back.set.map((entry) => [entry.path, entry]),
|
|
3297
|
-
...back.removed.map((path) => [path, null]),
|
|
3298
|
-
]);
|
|
3299
|
-
const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
|
|
3300
|
-
if (refused !== null) {
|
|
3301
|
-
const key = pendingKey(room.locator, submitted.id);
|
|
3302
|
-
return {
|
|
3303
|
-
locator: room.locator,
|
|
3304
|
-
left: [],
|
|
3305
|
-
commit: {
|
|
3306
|
-
rooms: [],
|
|
3307
|
-
writes: [
|
|
3308
|
-
{
|
|
3309
|
-
locator: room.locator,
|
|
3310
|
-
accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
|
|
3311
|
-
},
|
|
3312
|
-
],
|
|
3313
|
-
},
|
|
3314
|
-
refused,
|
|
3315
|
-
apply() {
|
|
3316
|
-
const state = pendings.get(key);
|
|
3317
|
-
if (state !== undefined && !fences.has(room.locator))
|
|
3318
|
-
pendings.set(key, { ...state, accepted: accepted.roomVersion });
|
|
3319
|
-
return changeSet(room.locator, { queue: true });
|
|
3320
|
-
},
|
|
3321
|
-
};
|
|
3322
|
-
}
|
|
3323
3466
|
changes = back;
|
|
3324
3467
|
}
|
|
3325
3468
|
const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
@@ -3327,10 +3470,9 @@ export function composeRuntime(options) {
|
|
|
3327
3470
|
...settled,
|
|
3328
3471
|
apply(answer) {
|
|
3329
3472
|
const set = settled.apply(answer);
|
|
3473
|
+
showing(room.locator, accepted.roomVersion);
|
|
3330
3474
|
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3331
3475
|
unreadable.delete(room.locator);
|
|
3332
|
-
sourceRefusals.delete(room.locator);
|
|
3333
|
-
declarationRefusals.delete(room.locator);
|
|
3334
3476
|
list(room, true);
|
|
3335
3477
|
return set;
|
|
3336
3478
|
},
|
|
@@ -3348,13 +3490,9 @@ export function composeRuntime(options) {
|
|
|
3348
3490
|
const failed = await exclusive(async () => {
|
|
3349
3491
|
const adopting = await acceptance(room, local, submitted, accepted);
|
|
3350
3492
|
await adoptedTogether(local, [adopting]);
|
|
3351
|
-
return
|
|
3493
|
+
return failureOf(adopting);
|
|
3352
3494
|
});
|
|
3353
|
-
|
|
3354
|
-
return true;
|
|
3355
|
-
if (failed.kind === 'refused' && continuing())
|
|
3356
|
-
refusedSource(room, failed);
|
|
3357
|
-
return false;
|
|
3495
|
+
return failed === null;
|
|
3358
3496
|
}
|
|
3359
3497
|
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
3360
3498
|
const other = discovered.get(partner);
|
|
@@ -3368,18 +3506,10 @@ export function composeRuntime(options) {
|
|
|
3368
3506
|
...accepted.destination,
|
|
3369
3507
|
});
|
|
3370
3508
|
await adoptedTogether(local, [left, arrived]);
|
|
3371
|
-
|
|
3372
|
-
if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
|
|
3373
|
-
refusedSource(other, refusedArrival);
|
|
3374
|
-
}
|
|
3375
|
-
return refusalOf(left);
|
|
3509
|
+
return failureOf(left);
|
|
3376
3510
|
});
|
|
3377
3511
|
submitLater(partner);
|
|
3378
|
-
|
|
3379
|
-
return true;
|
|
3380
|
-
if (failed.kind === 'refused' && continuing())
|
|
3381
|
-
refusedSource(room, failed);
|
|
3382
|
-
return false;
|
|
3512
|
+
return failed === null;
|
|
3383
3513
|
}
|
|
3384
3514
|
function queueIn(locator) {
|
|
3385
3515
|
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
@@ -3502,8 +3632,6 @@ export function composeRuntime(options) {
|
|
|
3502
3632
|
if (replica === undefined || head === undefined)
|
|
3503
3633
|
return 'done';
|
|
3504
3634
|
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
3505
|
-
if (state?.accepted !== undefined)
|
|
3506
|
-
return 'done';
|
|
3507
3635
|
const partner = partnerOf(head, room.locator);
|
|
3508
3636
|
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3509
3637
|
submitLater(partner);
|
|
@@ -3617,12 +3745,12 @@ export function composeRuntime(options) {
|
|
|
3617
3745
|
}
|
|
3618
3746
|
if (result.kind === 'expired') {
|
|
3619
3747
|
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
3620
|
-
const there = other === undefined ? null : await
|
|
3748
|
+
const there = other === undefined ? null : await advanced(other, local, authorized);
|
|
3621
3749
|
if (other !== undefined && there !== null) {
|
|
3622
3750
|
await withhold(there, other, local, progress);
|
|
3623
3751
|
return 'done';
|
|
3624
3752
|
}
|
|
3625
|
-
const withheld = await
|
|
3753
|
+
const withheld = await advanced(room, local, authorized);
|
|
3626
3754
|
if (withheld !== null) {
|
|
3627
3755
|
await withhold(withheld, room, local, progress);
|
|
3628
3756
|
return 'done';
|
|
@@ -3644,8 +3772,8 @@ export function composeRuntime(options) {
|
|
|
3644
3772
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
3645
3773
|
const left = leaving(head, result);
|
|
3646
3774
|
const refreshed = async () => ({
|
|
3647
|
-
there: other === undefined ? null : await
|
|
3648
|
-
here: await
|
|
3775
|
+
there: other === undefined ? null : await advanced(other, local, authorized),
|
|
3776
|
+
here: await advanced(room, local, authorized),
|
|
3649
3777
|
});
|
|
3650
3778
|
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
3651
3779
|
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
@@ -3716,6 +3844,7 @@ export function composeRuntime(options) {
|
|
|
3716
3844
|
locator: room.locator,
|
|
3717
3845
|
key: before.record.key,
|
|
3718
3846
|
roots: before.record.typedSelection?.roots ?? [],
|
|
3847
|
+
composed: composedOf(composed(before.record)),
|
|
3719
3848
|
overlay: { set: [], cleared: [...before.overlaid] },
|
|
3720
3849
|
},
|
|
3721
3850
|
]),
|
|
@@ -3748,39 +3877,61 @@ export function composeRuntime(options) {
|
|
|
3748
3877
|
if (signal.aborted)
|
|
3749
3878
|
return;
|
|
3750
3879
|
const progress = { complete: true, reachable: true };
|
|
3751
|
-
await roomTask(room.locator,
|
|
3752
|
-
await Promise.race([
|
|
3753
|
-
reload(room, local, progress, () => live() && !signal.aborted),
|
|
3754
|
-
abandonedOn(signal),
|
|
3755
|
-
]);
|
|
3756
|
-
});
|
|
3880
|
+
await roomTask(room.locator, (live) => reload(room, local, progress, live));
|
|
3757
3881
|
if (signal.aborted)
|
|
3758
3882
|
return;
|
|
3759
3883
|
await rest(progress);
|
|
3760
3884
|
}
|
|
3761
3885
|
}
|
|
3762
3886
|
function awaitingAnswer(locator) {
|
|
3887
|
+
answering.get(locator)?.release();
|
|
3763
3888
|
let release = () => { };
|
|
3764
|
-
const
|
|
3889
|
+
const answered = new Promise((resolve) => {
|
|
3765
3890
|
release = resolve;
|
|
3766
3891
|
});
|
|
3767
|
-
|
|
3892
|
+
const awaited = { answered, release };
|
|
3893
|
+
answering.set(locator, awaited);
|
|
3768
3894
|
return () => {
|
|
3769
|
-
if (answering.get(locator)
|
|
3895
|
+
if (answering.get(locator) === awaited)
|
|
3770
3896
|
answering.delete(locator);
|
|
3771
3897
|
release();
|
|
3772
3898
|
};
|
|
3773
3899
|
}
|
|
3774
|
-
|
|
3775
|
-
const
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3900
|
+
function declaredAsHeld(room) {
|
|
3901
|
+
const held = replicas.get(room.locator)?.record;
|
|
3902
|
+
return (held === undefined ||
|
|
3903
|
+
(JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
|
|
3904
|
+
JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
|
|
3905
|
+
JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
|
|
3906
|
+
}
|
|
3907
|
+
async function keepNaming(local, room) {
|
|
3908
|
+
const held = replicas.get(room.locator)?.record;
|
|
3909
|
+
if (held === undefined || (held.name === room.name && held.handle === room.handle))
|
|
3910
|
+
return;
|
|
3911
|
+
await exclusive(async () => {
|
|
3912
|
+
if (!continuing())
|
|
3913
|
+
return;
|
|
3914
|
+
const replica = replicas.get(room.locator);
|
|
3915
|
+
if (replica === undefined)
|
|
3916
|
+
return;
|
|
3917
|
+
const record = { ...replica.record, name: room.name, handle: room.handle };
|
|
3918
|
+
await local.commit({
|
|
3919
|
+
rooms: [
|
|
3920
|
+
{
|
|
3921
|
+
locator: room.locator,
|
|
3922
|
+
key: record.key,
|
|
3923
|
+
record: storedRecord(record),
|
|
3924
|
+
composed: composedOf(composed(record)),
|
|
3925
|
+
},
|
|
3926
|
+
],
|
|
3927
|
+
});
|
|
3928
|
+
replicas.set(room.locator, { ...replica, record });
|
|
3929
|
+
});
|
|
3930
|
+
}
|
|
3931
|
+
async function retainModel(local, room, next) {
|
|
3932
|
+
const key = room.locator;
|
|
3933
|
+
const previous = modelOf(room.locator);
|
|
3934
|
+
if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
|
|
3784
3935
|
return;
|
|
3785
3936
|
if (next.status === 'unavailable') {
|
|
3786
3937
|
invalidModels.add(key);
|
|
@@ -3792,26 +3943,15 @@ export function composeRuntime(options) {
|
|
|
3792
3943
|
const held = replicas.get(room.locator);
|
|
3793
3944
|
let changed = null;
|
|
3794
3945
|
if (held !== undefined) {
|
|
3795
|
-
const
|
|
3946
|
+
const record = {
|
|
3796
3947
|
...held.record,
|
|
3948
|
+
name: room.name,
|
|
3949
|
+
handle: room.handle,
|
|
3797
3950
|
operations: room.operations,
|
|
3798
3951
|
replica: room.replica,
|
|
3799
3952
|
typedSelection: room.typedSelection ?? null,
|
|
3800
|
-
|
|
3953
|
+
model: next,
|
|
3801
3954
|
};
|
|
3802
|
-
let read = 'staged';
|
|
3803
|
-
if (!sameMembership(declared, held.record)) {
|
|
3804
|
-
const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
|
|
3805
|
-
read =
|
|
3806
|
-
verdict.verdict !== 'refuse'
|
|
3807
|
-
? 'staged'
|
|
3808
|
-
: verdict.rule === 'model-unavailable'
|
|
3809
|
-
? 'deferred'
|
|
3810
|
-
: { kind: 'refused', refusal: verdict };
|
|
3811
|
-
}
|
|
3812
|
-
const record = read === 'staged'
|
|
3813
|
-
? declared
|
|
3814
|
-
: { ...declared, typedSelection: held.record.typedSelection };
|
|
3815
3955
|
const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
|
|
3816
3956
|
const replayedLayer = await replayed(record, confirmedLayer, held.queue);
|
|
3817
3957
|
const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
|
|
@@ -3822,7 +3962,7 @@ export function composeRuntime(options) {
|
|
|
3822
3962
|
key: record.key,
|
|
3823
3963
|
record: storedRecord(record),
|
|
3824
3964
|
roots: record.typedSelection?.roots ?? [],
|
|
3825
|
-
|
|
3965
|
+
composed: composedOf(composed(record)),
|
|
3826
3966
|
overlay: diff.overlay,
|
|
3827
3967
|
},
|
|
3828
3968
|
],
|
|
@@ -3833,29 +3973,22 @@ export function composeRuntime(options) {
|
|
|
3833
3973
|
documents: answer.changed[room.locator] ?? [],
|
|
3834
3974
|
room: true,
|
|
3835
3975
|
});
|
|
3836
|
-
if (read === 'staged' || read === 'deferred') {
|
|
3837
|
-
if (declarationRefusals.delete(room.locator)) {
|
|
3838
|
-
sourceRefusals.delete(room.locator);
|
|
3839
|
-
unreadable.delete(room.locator);
|
|
3840
|
-
}
|
|
3841
|
-
}
|
|
3842
|
-
else {
|
|
3843
|
-
refusedSource(room, read);
|
|
3844
|
-
declarationRefusals.add(room.locator);
|
|
3845
|
-
}
|
|
3846
|
-
}
|
|
3847
|
-
models.set(key, next);
|
|
3848
|
-
if (next.status === 'current') {
|
|
3849
|
-
liveModels.set(key, JSON.stringify(next.snapshot));
|
|
3850
|
-
invalidModels.delete(key);
|
|
3851
3976
|
}
|
|
3852
|
-
|
|
3977
|
+
heldModel(room, next);
|
|
3853
3978
|
if (changed === null)
|
|
3854
3979
|
emitRooms();
|
|
3855
3980
|
else
|
|
3856
3981
|
publishChangeSet(changed);
|
|
3857
3982
|
});
|
|
3858
3983
|
}
|
|
3984
|
+
function heldModel(room, next) {
|
|
3985
|
+
models.set(room.locator, next);
|
|
3986
|
+
if (next.status === 'current') {
|
|
3987
|
+
liveModels.set(room.locator, JSON.stringify(next.snapshot));
|
|
3988
|
+
invalidModels.delete(room.locator);
|
|
3989
|
+
}
|
|
3990
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
3991
|
+
}
|
|
3859
3992
|
async function scoped(result, room, local) {
|
|
3860
3993
|
if (result.scope === 'content' && !result.retryable) {
|
|
3861
3994
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
@@ -3869,14 +4002,14 @@ export function composeRuntime(options) {
|
|
|
3869
4002
|
});
|
|
3870
4003
|
let scopedAway = room;
|
|
3871
4004
|
if (result.scope === 'model')
|
|
3872
|
-
await retainModel(local, room,
|
|
4005
|
+
await retainModel(local, room, {
|
|
3873
4006
|
status: 'unavailable',
|
|
3874
4007
|
reason: 'not-readable',
|
|
3875
4008
|
});
|
|
3876
4009
|
else if (result.scope !== 'operation') {
|
|
3877
4010
|
unreadable.add(room.locator);
|
|
3878
4011
|
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
3879
|
-
await retainModel(local, scopedAway,
|
|
4012
|
+
await retainModel(local, scopedAway, modelOf(room.locator));
|
|
3880
4013
|
}
|
|
3881
4014
|
if (!continuing())
|
|
3882
4015
|
return;
|
|
@@ -3884,6 +4017,17 @@ export function composeRuntime(options) {
|
|
|
3884
4017
|
emitRooms();
|
|
3885
4018
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
3886
4019
|
}
|
|
4020
|
+
function name(locator) {
|
|
4021
|
+
const stream = options.adapter.roomStream;
|
|
4022
|
+
const record = replicas.get(locator)?.record;
|
|
4023
|
+
const room = discovered.get(locator);
|
|
4024
|
+
if (stream === undefined || record === undefined || room === undefined)
|
|
4025
|
+
return;
|
|
4026
|
+
if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
|
|
4027
|
+
return;
|
|
4028
|
+
naming += 1;
|
|
4029
|
+
stream.hold([{ locator, roomVersion: record.roomVersion }]);
|
|
4030
|
+
}
|
|
3887
4031
|
function heldRooms(listed) {
|
|
3888
4032
|
return listed.flatMap((room) => {
|
|
3889
4033
|
const record = replicas.get(room.locator)?.record;
|
|
@@ -3901,14 +4045,19 @@ export function composeRuntime(options) {
|
|
|
3901
4045
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
3902
4046
|
});
|
|
3903
4047
|
if (attempt === 'opened') {
|
|
4048
|
+
naming += 1;
|
|
3904
4049
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
3905
4050
|
handed = true;
|
|
3906
4051
|
}
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
4052
|
+
for (const room of ready) {
|
|
4053
|
+
if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
|
|
4054
|
+
if (attempt === 'authorized')
|
|
4055
|
+
name(room.locator);
|
|
4056
|
+
continue;
|
|
4057
|
+
}
|
|
4058
|
+
void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
|
|
4059
|
+
}
|
|
4060
|
+
const failed = prepared.find((room) => room.status === 'rejected');
|
|
3912
4061
|
if (failed !== undefined)
|
|
3913
4062
|
throw failed.reason;
|
|
3914
4063
|
}
|
|
@@ -3926,27 +4075,31 @@ export function composeRuntime(options) {
|
|
|
3926
4075
|
return false;
|
|
3927
4076
|
}
|
|
3928
4077
|
if (prior !== undefined)
|
|
3929
|
-
await retainModel(local, room,
|
|
4078
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
4079
|
+
if (prior !== undefined)
|
|
4080
|
+
await keepNaming(local, room);
|
|
3930
4081
|
if (discoveryPaused || authorization === undefined)
|
|
3931
4082
|
return false;
|
|
3932
4083
|
return room.replica.admission === 'admitted';
|
|
3933
4084
|
}
|
|
3934
4085
|
async function caughtUp(room, local, progress, live) {
|
|
3935
|
-
if (
|
|
4086
|
+
if (options.adapter.roomStream !== undefined || authorization === undefined)
|
|
4087
|
+
return;
|
|
4088
|
+
if (!live() || !behind(room))
|
|
3936
4089
|
return;
|
|
3937
4090
|
const withheld = await advance(room, local, authorization, live);
|
|
3938
4091
|
if (withheld !== null)
|
|
3939
4092
|
await withhold(withheld, room, local, progress);
|
|
3940
4093
|
}
|
|
3941
4094
|
function revoked(locator) {
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3945
|
-
});
|
|
4095
|
+
const model = modelOf(locator);
|
|
4096
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3946
4097
|
}
|
|
3947
4098
|
async function loadRoom(room, local, progress, live) {
|
|
3948
4099
|
if (!live() || authorization === undefined)
|
|
3949
4100
|
return;
|
|
4101
|
+
if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
|
|
4102
|
+
return;
|
|
3950
4103
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
3951
4104
|
await reload(room, local, progress, live);
|
|
3952
4105
|
return;
|
|
@@ -3964,20 +4117,53 @@ export function composeRuntime(options) {
|
|
|
3964
4117
|
reloadsDue.delete(room.locator);
|
|
3965
4118
|
if (!live() || authorization === undefined)
|
|
3966
4119
|
return;
|
|
4120
|
+
const from = standingOf(room.locator);
|
|
3967
4121
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
3968
4122
|
if (bootstrapped.kind !== 'staged') {
|
|
3969
4123
|
if (live())
|
|
3970
4124
|
await withhold(bootstrapped, room, local, progress);
|
|
3971
4125
|
return;
|
|
3972
4126
|
}
|
|
3973
|
-
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
4127
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
|
|
3974
4128
|
await caughtUp(room, local, progress, live);
|
|
3975
4129
|
}
|
|
3976
4130
|
finally {
|
|
3977
4131
|
caughtUpTo(room.locator);
|
|
3978
4132
|
}
|
|
3979
4133
|
}
|
|
3980
|
-
async function
|
|
4134
|
+
async function asked(authorized) {
|
|
4135
|
+
try {
|
|
4136
|
+
return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
|
|
4137
|
+
}
|
|
4138
|
+
catch {
|
|
4139
|
+
return { kind: 'transport-uncertainty' };
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
function malformed(listed) {
|
|
4143
|
+
for (const room of listed) {
|
|
4144
|
+
if (room.operations === undefined ||
|
|
4145
|
+
!['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
4146
|
+
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
4147
|
+
room.replica === undefined ||
|
|
4148
|
+
typeof room.replica.selection !== 'string' ||
|
|
4149
|
+
room.replica.selection.length === 0 ||
|
|
4150
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
4151
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
4152
|
+
return `invalid model or replica declaration ${room.locator}`;
|
|
4153
|
+
}
|
|
4154
|
+
if (!declaredSelection(room))
|
|
4155
|
+
return `invalid typed selection declaration ${room.locator}`;
|
|
4156
|
+
}
|
|
4157
|
+
return null;
|
|
4158
|
+
}
|
|
4159
|
+
async function signIn(local, signedIn) {
|
|
4160
|
+
if (JSON.stringify(signedIn) === JSON.stringify(member))
|
|
4161
|
+
return;
|
|
4162
|
+
await local.fact('member', signedIn);
|
|
4163
|
+
member = signedIn;
|
|
4164
|
+
emitMember();
|
|
4165
|
+
}
|
|
4166
|
+
async function discoverRooms(local, progress, attempt, live, found) {
|
|
3981
4167
|
if (closed || authorization === undefined) {
|
|
3982
4168
|
notifyReady();
|
|
3983
4169
|
return;
|
|
@@ -3985,13 +4171,9 @@ export function composeRuntime(options) {
|
|
|
3985
4171
|
moveTo(transferring(status));
|
|
3986
4172
|
discoveries += 1;
|
|
3987
4173
|
const started = discoveries;
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
}
|
|
3992
|
-
catch {
|
|
3993
|
-
result = { kind: 'transport-uncertainty' };
|
|
3994
|
-
}
|
|
4174
|
+
const result = found ?? (await asked(authorization));
|
|
4175
|
+
if (result.kind === 'discovery')
|
|
4176
|
+
roomsNamed = true;
|
|
3995
4177
|
if (!live()) {
|
|
3996
4178
|
progress.complete = false;
|
|
3997
4179
|
notifyReady();
|
|
@@ -3999,12 +4181,7 @@ export function composeRuntime(options) {
|
|
|
3999
4181
|
}
|
|
4000
4182
|
journal.record(`connection discover ${result.kind}`);
|
|
4001
4183
|
if (result.kind === 'transport-uncertainty') {
|
|
4002
|
-
|
|
4003
|
-
await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
|
|
4004
|
-
if (room.typedSelection !== null) {
|
|
4005
|
-
await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
|
|
4006
|
-
}
|
|
4007
|
-
}
|
|
4184
|
+
await cachedModels(local);
|
|
4008
4185
|
progress.reachable = false;
|
|
4009
4186
|
progress.complete = false;
|
|
4010
4187
|
notifyReady();
|
|
@@ -4025,26 +4202,12 @@ export function composeRuntime(options) {
|
|
|
4025
4202
|
notifyReady();
|
|
4026
4203
|
return;
|
|
4027
4204
|
}
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
room.replica.selection.length === 0 ||
|
|
4035
|
-
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
4036
|
-
!['locked', 'open'].includes(room.replica.offline)) {
|
|
4037
|
-
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
4038
|
-
await refuse('missing-required-guarantee');
|
|
4039
|
-
notifyReady();
|
|
4040
|
-
return;
|
|
4041
|
-
}
|
|
4042
|
-
if (!declaredSelection(room)) {
|
|
4043
|
-
journal.record(`invalid typed selection declaration ${room.locator}`);
|
|
4044
|
-
await refuse('missing-required-guarantee');
|
|
4045
|
-
notifyReady();
|
|
4046
|
-
return;
|
|
4047
|
-
}
|
|
4205
|
+
const invalid = malformed(result.rooms);
|
|
4206
|
+
if (invalid !== null) {
|
|
4207
|
+
journal.record(invalid);
|
|
4208
|
+
await refuse('missing-required-guarantee');
|
|
4209
|
+
notifyReady();
|
|
4210
|
+
return;
|
|
4048
4211
|
}
|
|
4049
4212
|
if (refusal !== null) {
|
|
4050
4213
|
refusal = null;
|
|
@@ -4060,13 +4223,8 @@ export function composeRuntime(options) {
|
|
|
4060
4223
|
await local.fact('providerLimits', limits);
|
|
4061
4224
|
await local.fact('capabilities', capabilities);
|
|
4062
4225
|
}
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
await local.fact('member', signedIn);
|
|
4066
|
-
member = signedIn;
|
|
4067
|
-
emitMember();
|
|
4068
|
-
}
|
|
4069
|
-
await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
|
|
4226
|
+
await signIn(local, result.member ?? null);
|
|
4227
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
4070
4228
|
}
|
|
4071
4229
|
async function ensureAuthorization() {
|
|
4072
4230
|
if (authorization !== undefined)
|
|
@@ -4081,23 +4239,45 @@ export function composeRuntime(options) {
|
|
|
4081
4239
|
}
|
|
4082
4240
|
}
|
|
4083
4241
|
async function admittedRead(work) {
|
|
4084
|
-
|
|
4085
|
-
|
|
4242
|
+
while (roomReads.free === 0) {
|
|
4243
|
+
await new Promise((admit) => roomReads.waiting.push(admit));
|
|
4086
4244
|
}
|
|
4087
|
-
|
|
4088
|
-
while (reads.free === 0)
|
|
4089
|
-
await new Promise((admit) => reads.waiting.push(admit));
|
|
4090
|
-
reads.free -= 1;
|
|
4245
|
+
roomReads.free -= 1;
|
|
4091
4246
|
try {
|
|
4092
4247
|
await work();
|
|
4093
4248
|
}
|
|
4094
4249
|
finally {
|
|
4095
|
-
|
|
4096
|
-
|
|
4250
|
+
roomReads.free += 1;
|
|
4251
|
+
roomReads.waiting.shift()?.();
|
|
4097
4252
|
}
|
|
4098
4253
|
}
|
|
4254
|
+
function inRoom(locator, work) {
|
|
4255
|
+
roomWork += 1;
|
|
4256
|
+
const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
|
|
4257
|
+
try {
|
|
4258
|
+
return await work(continuing);
|
|
4259
|
+
}
|
|
4260
|
+
finally {
|
|
4261
|
+
roomWork -= 1;
|
|
4262
|
+
}
|
|
4263
|
+
});
|
|
4264
|
+
const chain = ran.then(() => undefined, () => undefined);
|
|
4265
|
+
roomLanes.set(locator, chain);
|
|
4266
|
+
working.add(chain);
|
|
4267
|
+
void chain.finally(() => {
|
|
4268
|
+
working.delete(chain);
|
|
4269
|
+
if (roomLanes.get(locator) === chain)
|
|
4270
|
+
roomLanes.delete(locator);
|
|
4271
|
+
});
|
|
4272
|
+
return ran;
|
|
4273
|
+
}
|
|
4099
4274
|
function roomTask(locator, work) {
|
|
4100
|
-
|
|
4275
|
+
if (stopping)
|
|
4276
|
+
return Promise.resolve();
|
|
4277
|
+
return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
|
|
4278
|
+
}
|
|
4279
|
+
function advanced(room, local, authorized) {
|
|
4280
|
+
return inRoom(room.locator, (live) => advance(room, local, authorized, live));
|
|
4101
4281
|
}
|
|
4102
4282
|
function task(lane, work) {
|
|
4103
4283
|
if (stopping)
|
|
@@ -4113,23 +4293,19 @@ export function composeRuntime(options) {
|
|
|
4113
4293
|
void run.finally(() => working.delete(run));
|
|
4114
4294
|
return run;
|
|
4115
4295
|
}
|
|
4116
|
-
function sync(attempt) {
|
|
4296
|
+
function sync(attempt, found) {
|
|
4117
4297
|
return task(OPENING_LANE, async (live) => {
|
|
4118
4298
|
if (!live()) {
|
|
4119
|
-
|
|
4299
|
+
readyFromStore();
|
|
4120
4300
|
return;
|
|
4121
4301
|
}
|
|
4122
4302
|
const progress = { complete: true, reachable: true };
|
|
4123
|
-
|
|
4124
|
-
if (attempt === 'opened')
|
|
4125
|
-
opened = generation;
|
|
4126
|
-
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
4303
|
+
underway = attempt === 'opened' ? progress : null;
|
|
4127
4304
|
if (attempt === 'opened')
|
|
4128
4305
|
streamed = progress;
|
|
4129
4306
|
complete = false;
|
|
4130
4307
|
recorded = false;
|
|
4131
4308
|
moveTo(transferring(status));
|
|
4132
|
-
handed = false;
|
|
4133
4309
|
try {
|
|
4134
4310
|
const local = await openLocal();
|
|
4135
4311
|
if (!live())
|
|
@@ -4137,7 +4313,7 @@ export function composeRuntime(options) {
|
|
|
4137
4313
|
await ensureAuthorization();
|
|
4138
4314
|
if (!live())
|
|
4139
4315
|
return;
|
|
4140
|
-
await discoverRooms(local, progress, attempt, live);
|
|
4316
|
+
await discoverRooms(local, progress, attempt, live, found);
|
|
4141
4317
|
}
|
|
4142
4318
|
catch {
|
|
4143
4319
|
progress.complete = false;
|
|
@@ -4145,7 +4321,6 @@ export function composeRuntime(options) {
|
|
|
4145
4321
|
progress.reachable = false;
|
|
4146
4322
|
}
|
|
4147
4323
|
finally {
|
|
4148
|
-
gate.release();
|
|
4149
4324
|
notifyReady();
|
|
4150
4325
|
}
|
|
4151
4326
|
if (!live())
|
|
@@ -4157,35 +4332,59 @@ export function composeRuntime(options) {
|
|
|
4157
4332
|
if (handed || closed || progress.reachable)
|
|
4158
4333
|
return;
|
|
4159
4334
|
underway = null;
|
|
4160
|
-
opened = null;
|
|
4161
|
-
journal.record('connection reopen');
|
|
4162
4335
|
await rest(progress, live);
|
|
4163
|
-
options.adapter.roomStream?.reopen();
|
|
4164
4336
|
});
|
|
4165
4337
|
}
|
|
4166
|
-
function
|
|
4338
|
+
function resynchronize() {
|
|
4167
4339
|
return task(OPENING_LANE, async (live) => {
|
|
4168
|
-
if (!live() || !
|
|
4340
|
+
if (!live() || !announced)
|
|
4169
4341
|
return;
|
|
4170
4342
|
const progress = { complete: true, reachable: true };
|
|
4171
|
-
const gate = discover();
|
|
4172
4343
|
moveTo(transferring(status));
|
|
4173
4344
|
try {
|
|
4174
4345
|
const local = await openLocal();
|
|
4175
4346
|
if (!live())
|
|
4176
4347
|
return;
|
|
4177
|
-
await
|
|
4348
|
+
await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
|
|
4349
|
+
}
|
|
4350
|
+
catch {
|
|
4351
|
+
progress.complete = false;
|
|
4352
|
+
}
|
|
4353
|
+
if (live())
|
|
4354
|
+
await finishOpening(progress, live);
|
|
4355
|
+
});
|
|
4356
|
+
}
|
|
4357
|
+
function countAuthorityChange() {
|
|
4358
|
+
authorityChanges += 1;
|
|
4359
|
+
for (const listener of authorityChangeListeners)
|
|
4360
|
+
listener(authorityChanges);
|
|
4361
|
+
}
|
|
4362
|
+
function changeAuthority(change) {
|
|
4363
|
+
return task(OPENING_LANE, async (live) => {
|
|
4364
|
+
if (!live() || !announced)
|
|
4365
|
+
return;
|
|
4366
|
+
countAuthorityChange();
|
|
4367
|
+
const progress = { complete: true, reachable: true };
|
|
4368
|
+
moveTo(transferring(status));
|
|
4369
|
+
try {
|
|
4370
|
+
const local = await openLocal();
|
|
4178
4371
|
if (!live())
|
|
4179
4372
|
return;
|
|
4180
|
-
|
|
4373
|
+
const invalid = malformed(change.rooms);
|
|
4374
|
+
if (invalid !== null) {
|
|
4375
|
+
journal.record(invalid);
|
|
4376
|
+
await refuse('missing-required-guarantee');
|
|
4377
|
+
return;
|
|
4378
|
+
}
|
|
4379
|
+
if (change.member !== undefined)
|
|
4380
|
+
await signIn(local, change.member);
|
|
4381
|
+
discoveries += 1;
|
|
4382
|
+
const listed = await applyDiscovery(change.rooms, discoveries, change.left);
|
|
4383
|
+
await synchronize(listed, local, progress, 'authorized', live);
|
|
4181
4384
|
}
|
|
4182
4385
|
catch {
|
|
4183
4386
|
progress.complete = false;
|
|
4184
4387
|
}
|
|
4185
|
-
finally {
|
|
4186
|
-
gate.release();
|
|
4187
|
-
notifyReady();
|
|
4188
|
-
}
|
|
4189
4388
|
if (!live())
|
|
4190
4389
|
return;
|
|
4191
4390
|
if (!progress.complete)
|
|
@@ -4193,14 +4392,17 @@ export function composeRuntime(options) {
|
|
|
4193
4392
|
await rest(progress, live);
|
|
4194
4393
|
});
|
|
4195
4394
|
}
|
|
4196
|
-
function closedSocket(revoked, lost) {
|
|
4197
|
-
|
|
4395
|
+
function closedSocket(revoked, lost, authorityChanged) {
|
|
4396
|
+
notifyReady();
|
|
4397
|
+
const opened = announced;
|
|
4198
4398
|
socketOpen = false;
|
|
4399
|
+
announced = false;
|
|
4400
|
+
missedChange ||= authorityChanged;
|
|
4401
|
+
handed = false;
|
|
4402
|
+
naming = 0;
|
|
4199
4403
|
connectionLost();
|
|
4200
|
-
if (lost)
|
|
4201
|
-
reads.lost();
|
|
4404
|
+
if (lost)
|
|
4202
4405
|
conversations.lost();
|
|
4203
|
-
}
|
|
4204
4406
|
if (revoked) {
|
|
4205
4407
|
void refuse('revoked-authorization');
|
|
4206
4408
|
return;
|
|
@@ -4211,18 +4413,32 @@ export function composeRuntime(options) {
|
|
|
4211
4413
|
moveTo({
|
|
4212
4414
|
...status,
|
|
4213
4415
|
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
4214
|
-
providerUnreachable: status.providerUnreachable || !opened,
|
|
4416
|
+
providerUnreachable: status.providerUnreachable || (!opened && lost),
|
|
4417
|
+
});
|
|
4418
|
+
void closing(opened);
|
|
4419
|
+
}
|
|
4420
|
+
function closing(opened) {
|
|
4421
|
+
return task(OPENING_LANE, async (live) => {
|
|
4422
|
+
if (!live())
|
|
4423
|
+
return;
|
|
4424
|
+
const local = await openLocal();
|
|
4425
|
+
if (!opened && !socketOpen)
|
|
4426
|
+
await cachedModels(local);
|
|
4427
|
+
await rest(null, live);
|
|
4215
4428
|
});
|
|
4216
4429
|
}
|
|
4217
|
-
function
|
|
4218
|
-
|
|
4430
|
+
function cachedModels(local) {
|
|
4431
|
+
return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
|
|
4432
|
+
}
|
|
4433
|
+
function caughtUpRooms(expired) {
|
|
4434
|
+
const progress = underway;
|
|
4435
|
+
underway = null;
|
|
4436
|
+
naming = Math.max(0, naming - 1);
|
|
4219
4437
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
4220
|
-
|
|
4221
|
-
opened = null;
|
|
4438
|
+
replaced(expired);
|
|
4222
4439
|
const reloading = progress === null ? [] : owedReloads(expired);
|
|
4223
4440
|
const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
|
|
4224
4441
|
return task(OPENING_LANE, async (live) => {
|
|
4225
|
-
underway = null;
|
|
4226
4442
|
if (!live())
|
|
4227
4443
|
return;
|
|
4228
4444
|
if (progress === null) {
|
|
@@ -4230,24 +4446,14 @@ export function composeRuntime(options) {
|
|
|
4230
4446
|
await rest(cycle, live);
|
|
4231
4447
|
return;
|
|
4232
4448
|
}
|
|
4233
|
-
await roomsCarried([...expired, ...reloading]);
|
|
4234
|
-
if (!live())
|
|
4235
|
-
return;
|
|
4236
4449
|
try {
|
|
4237
4450
|
const local = await openLocal();
|
|
4238
|
-
|
|
4451
|
+
await replacing;
|
|
4239
4452
|
if (progress.reachable) {
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
!reloading.includes(room.locator)) {
|
|
4244
|
-
await confirmModels(room, local);
|
|
4245
|
-
}
|
|
4246
|
-
}
|
|
4453
|
+
await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
|
|
4454
|
+
!expired.includes(room.locator) &&
|
|
4455
|
+
!reloading.includes(room.locator)), 'current', local);
|
|
4247
4456
|
}
|
|
4248
|
-
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
4249
|
-
if (failed !== undefined)
|
|
4250
|
-
throw failed.reason;
|
|
4251
4457
|
}
|
|
4252
4458
|
catch {
|
|
4253
4459
|
progress.complete = false;
|
|
@@ -4259,11 +4465,8 @@ export function composeRuntime(options) {
|
|
|
4259
4465
|
}
|
|
4260
4466
|
function owedReloads(expired) {
|
|
4261
4467
|
const reloading = [...discovered.values()]
|
|
4262
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
4263
|
-
!expired.includes(room.locator) &&
|
|
4264
|
-
(owed.has(room.locator) || revoked(room.locator)))
|
|
4468
|
+
.filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
|
|
4265
4469
|
.map((room) => room.locator);
|
|
4266
|
-
owed.clear();
|
|
4267
4470
|
for (const locator of reloading)
|
|
4268
4471
|
catching.add(locator);
|
|
4269
4472
|
liveQueries.recover();
|
|
@@ -4272,24 +4475,43 @@ export function composeRuntime(options) {
|
|
|
4272
4475
|
async function reloadNow(replaced, progress) {
|
|
4273
4476
|
const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
|
|
4274
4477
|
for (const room of reloaded)
|
|
4275
|
-
reloadsDue.
|
|
4478
|
+
reloadsDue.add(room.locator);
|
|
4276
4479
|
try {
|
|
4277
4480
|
const local = await openLocal();
|
|
4278
|
-
|
|
4481
|
+
for (const room of reloaded) {
|
|
4482
|
+
void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
|
|
4483
|
+
}
|
|
4279
4484
|
}
|
|
4280
4485
|
catch {
|
|
4281
4486
|
for (const room of reloaded)
|
|
4282
4487
|
reloadsDue.delete(room.locator);
|
|
4283
4488
|
progress.reachable = false;
|
|
4284
|
-
return [];
|
|
4285
4489
|
}
|
|
4286
4490
|
}
|
|
4287
|
-
async function
|
|
4288
|
-
|
|
4289
|
-
const model = modelOf(room.locator
|
|
4290
|
-
|
|
4291
|
-
|
|
4491
|
+
async function settleModels(held, status, local) {
|
|
4492
|
+
const owed = (room) => {
|
|
4493
|
+
const model = modelOf(room.locator);
|
|
4494
|
+
return model.status !== 'unavailable' && model.status !== status;
|
|
4495
|
+
};
|
|
4496
|
+
const settled = await exclusive(async () => {
|
|
4497
|
+
if (!continuing())
|
|
4498
|
+
return [];
|
|
4499
|
+
const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
|
|
4500
|
+
for (const room of unchanged) {
|
|
4501
|
+
const next = { ...modelOf(room.locator), status };
|
|
4502
|
+
const replica = replicas.get(room.locator);
|
|
4503
|
+
if (replica !== undefined)
|
|
4504
|
+
replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
|
|
4505
|
+
heldModel(room, next);
|
|
4292
4506
|
}
|
|
4507
|
+
if (unchanged.length > 0)
|
|
4508
|
+
emitRooms();
|
|
4509
|
+
return unchanged;
|
|
4510
|
+
});
|
|
4511
|
+
for (const room of held) {
|
|
4512
|
+
if (settled.includes(room) || !owed(room))
|
|
4513
|
+
continue;
|
|
4514
|
+
await retainModel(local, room, { ...modelOf(room.locator), status });
|
|
4293
4515
|
}
|
|
4294
4516
|
}
|
|
4295
4517
|
function submitLater(locator) {
|
|
@@ -4334,14 +4556,10 @@ export function composeRuntime(options) {
|
|
|
4334
4556
|
try {
|
|
4335
4557
|
const local = await openLocal();
|
|
4336
4558
|
await roomTask(locator, async (live) => {
|
|
4337
|
-
|
|
4338
|
-
if (!current() || authorization === undefined)
|
|
4559
|
+
if (!live() || authorization === undefined)
|
|
4339
4560
|
return;
|
|
4340
|
-
const withheld = await
|
|
4341
|
-
|
|
4342
|
-
abandonedOn(signal),
|
|
4343
|
-
]);
|
|
4344
|
-
if (withheld !== null && current())
|
|
4561
|
+
const withheld = await advance(room, local, authorization, live);
|
|
4562
|
+
if (withheld !== null && live())
|
|
4345
4563
|
await withhold(withheld, room, local, progress);
|
|
4346
4564
|
});
|
|
4347
4565
|
}
|
|
@@ -4396,14 +4614,11 @@ export function composeRuntime(options) {
|
|
|
4396
4614
|
progress.complete = false;
|
|
4397
4615
|
return;
|
|
4398
4616
|
}
|
|
4399
|
-
reads.applied();
|
|
4400
4617
|
submitAll();
|
|
4401
4618
|
for (const locator of stale)
|
|
4402
4619
|
if (repairable.has(locator))
|
|
4403
4620
|
repairLater(locator, {});
|
|
4404
4621
|
await conversations.reconcile();
|
|
4405
|
-
if (status.providerUnreachable)
|
|
4406
|
-
conversations.reconnected();
|
|
4407
4622
|
const reading = { complete: true, reachable: true };
|
|
4408
4623
|
await conversations.catchUp(reading);
|
|
4409
4624
|
if (progress.reachable && reading.reachable)
|
|
@@ -4429,7 +4644,7 @@ export function composeRuntime(options) {
|
|
|
4429
4644
|
if (!live())
|
|
4430
4645
|
return;
|
|
4431
4646
|
let lastCompleted = status.lastCompleted;
|
|
4432
|
-
let synced = complete && stale.size === 0 && !pending();
|
|
4647
|
+
let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
|
|
4433
4648
|
if (synced && !recorded) {
|
|
4434
4649
|
const completedAt = new Date().toISOString();
|
|
4435
4650
|
try {
|
|
@@ -4444,74 +4659,78 @@ export function composeRuntime(options) {
|
|
|
4444
4659
|
}
|
|
4445
4660
|
}
|
|
4446
4661
|
moveTo({
|
|
4447
|
-
state: pending()
|
|
4662
|
+
state: pending()
|
|
4663
|
+
? 'pending'
|
|
4664
|
+
: synced && complete && roomWork === 0 && naming === 0
|
|
4665
|
+
? 'synced'
|
|
4666
|
+
: 'syncing',
|
|
4448
4667
|
lastCompleted,
|
|
4449
|
-
providerUnreachable: !progress.reachable,
|
|
4668
|
+
providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
|
|
4450
4669
|
...attention(),
|
|
4451
4670
|
});
|
|
4452
4671
|
}
|
|
4453
|
-
function
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
}
|
|
4461
|
-
function applyRoom(locator, roomVersion) {
|
|
4462
|
-
announced.set(locator, roomVersion);
|
|
4463
|
-
return task(roomLane(locator), async (live) => {
|
|
4464
|
-
if (discovering?.generation === generation)
|
|
4465
|
-
await discovering.done;
|
|
4466
|
-
const saving = answering.get(locator);
|
|
4467
|
-
if (saving?.generation === generation)
|
|
4468
|
-
await saving.answered;
|
|
4469
|
-
if (!live() || authorization === undefined)
|
|
4470
|
-
return;
|
|
4471
|
-
const room = discovered.get(locator);
|
|
4472
|
-
const held = replicas.get(locator);
|
|
4473
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
4474
|
-
return;
|
|
4475
|
-
if (held !== undefined &&
|
|
4476
|
-
(held.record.replica.selection !== room.replica.selection ||
|
|
4477
|
-
options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
|
|
4478
|
-
return;
|
|
4479
|
-
}
|
|
4480
|
-
const during = underway?.generation === generation ? underway.progress : null;
|
|
4481
|
-
const progress = during ?? { complete: true, reachable: true };
|
|
4482
|
-
if (during === null)
|
|
4483
|
-
moveTo(transferring(status));
|
|
4672
|
+
function applyRoom(change) {
|
|
4673
|
+
const { locator } = change;
|
|
4674
|
+
if (stopping)
|
|
4675
|
+
return Promise.resolve();
|
|
4676
|
+
return inRoom(locator, async (live) => {
|
|
4677
|
+
await answering.get(locator)?.answered;
|
|
4678
|
+
const progress = underway ?? { complete: true, reachable: true };
|
|
4484
4679
|
try {
|
|
4485
4680
|
const local = await openLocal();
|
|
4486
|
-
|
|
4487
|
-
if (!live()
|
|
4488
|
-
return;
|
|
4489
|
-
const
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4681
|
+
for (;;) {
|
|
4682
|
+
if (!live())
|
|
4683
|
+
return null;
|
|
4684
|
+
const room = discovered.get(locator);
|
|
4685
|
+
const held = replicas.get(locator);
|
|
4686
|
+
if (room === undefined ||
|
|
4687
|
+
held === undefined ||
|
|
4688
|
+
room.replica.admission !== 'admitted' ||
|
|
4689
|
+
held.record.replica.selection !== room.replica.selection) {
|
|
4690
|
+
return null;
|
|
4691
|
+
}
|
|
4692
|
+
const at = held.record.roomVersion;
|
|
4693
|
+
if (options.adapter.reached(locator, at, change.roomVersion))
|
|
4694
|
+
return progress;
|
|
4695
|
+
if (!options.adapter.reached(locator, at, change.from)) {
|
|
4696
|
+
name(locator);
|
|
4697
|
+
return progress;
|
|
4698
|
+
}
|
|
4699
|
+
if (underway === null)
|
|
4700
|
+
moveTo(transferring(status));
|
|
4701
|
+
const writing = () => live() && !full;
|
|
4702
|
+
let outlined = false;
|
|
4703
|
+
const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, async () => {
|
|
4704
|
+
const page = await change.read();
|
|
4705
|
+
const paths = Object.keys(page.outlines ?? {});
|
|
4706
|
+
outlined = paths.length > 0;
|
|
4707
|
+
await local.remember(locator, held.record.key, paths);
|
|
4708
|
+
return page;
|
|
4709
|
+
}));
|
|
4710
|
+
if (refreshed.kind !== 'advanced')
|
|
4711
|
+
return progress;
|
|
4712
|
+
const landing = {
|
|
4713
|
+
kind: 'refreshed',
|
|
4714
|
+
from: refreshed.from,
|
|
4715
|
+
remodelled: refreshed.remodelled,
|
|
4716
|
+
};
|
|
4717
|
+
if (await publish(local, room, refreshed.record, landing, writing)) {
|
|
4718
|
+
stale.delete(locator);
|
|
4719
|
+
await retainModel(local, room, modelOf(locator));
|
|
4720
|
+
if (outlined)
|
|
4721
|
+
void heldAgain(locator);
|
|
4722
|
+
return progress;
|
|
4723
|
+
}
|
|
4724
|
+
}
|
|
4493
4725
|
}
|
|
4494
4726
|
catch {
|
|
4727
|
+
if (!stale.has(locator))
|
|
4728
|
+
name(locator);
|
|
4495
4729
|
stale.add(locator);
|
|
4730
|
+
return progress;
|
|
4496
4731
|
}
|
|
4497
|
-
|
|
4498
|
-
if (during === null)
|
|
4499
|
-
await rest(progress, live);
|
|
4500
|
-
}
|
|
4501
|
-
});
|
|
4732
|
+
}).then((progress) => rest(progress), () => undefined);
|
|
4502
4733
|
}
|
|
4503
|
-
function roomsCarried(replacing) {
|
|
4504
|
-
const replaced = new Set(replacing.map(roomLane));
|
|
4505
|
-
return Promise.all([...lanes]
|
|
4506
|
-
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
4507
|
-
.map(([, held]) => held.chain));
|
|
4508
|
-
}
|
|
4509
|
-
const receipts = {
|
|
4510
|
-
heads: async () => (await openLocal()).receivedHeads(),
|
|
4511
|
-
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
4512
|
-
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
4513
|
-
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
4514
|
-
};
|
|
4515
4734
|
function observeStream() {
|
|
4516
4735
|
const stream = options.adapter.roomStream;
|
|
4517
4736
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -4521,29 +4740,35 @@ export function composeRuntime(options) {
|
|
|
4521
4740
|
return;
|
|
4522
4741
|
if (event.kind === 'opened') {
|
|
4523
4742
|
socketOpen = true;
|
|
4743
|
+
announced = false;
|
|
4744
|
+
handed = false;
|
|
4745
|
+
naming = 0;
|
|
4524
4746
|
connectionLive();
|
|
4525
4747
|
generation += 1;
|
|
4526
|
-
reads.opened();
|
|
4527
4748
|
conversations.reopened();
|
|
4528
4749
|
recovery.resume();
|
|
4529
|
-
|
|
4750
|
+
}
|
|
4751
|
+
else if (event.kind === 'discovered') {
|
|
4752
|
+
if (!socketOpen)
|
|
4753
|
+
return;
|
|
4754
|
+
announced = true;
|
|
4755
|
+
if (missedChange) {
|
|
4756
|
+
missedChange = false;
|
|
4757
|
+
countAuthorityChange();
|
|
4758
|
+
}
|
|
4759
|
+
void sync('opened', event.discovery);
|
|
4760
|
+
for (const locator of replicas.keys())
|
|
4761
|
+
void heldAgain(locator);
|
|
4530
4762
|
}
|
|
4531
4763
|
else if (event.kind === 'closed')
|
|
4532
|
-
closedSocket(event.revoked, event.lost);
|
|
4533
|
-
else if (event.kind === '
|
|
4534
|
-
void
|
|
4764
|
+
closedSocket(event.revoked, event.lost, event.authorityChanged);
|
|
4765
|
+
else if (event.kind === 'caught-up')
|
|
4766
|
+
void caughtUpRooms(event.expired);
|
|
4535
4767
|
else if (event.kind === 'authority-changed')
|
|
4536
|
-
void
|
|
4768
|
+
void changeAuthority(event);
|
|
4537
4769
|
else
|
|
4538
|
-
void applyRoom(event
|
|
4539
|
-
}
|
|
4540
|
-
}
|
|
4541
|
-
async function heldRevisions(locator, paths) {
|
|
4542
|
-
const held = replicas.get(locator);
|
|
4543
|
-
if (held === undefined || paths.length === 0)
|
|
4544
|
-
return new Map();
|
|
4545
|
-
const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
|
|
4546
|
-
return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
|
|
4770
|
+
void applyRoom(event);
|
|
4771
|
+
});
|
|
4547
4772
|
}
|
|
4548
4773
|
function writesHeld() {
|
|
4549
4774
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -4613,10 +4838,9 @@ export function composeRuntime(options) {
|
|
|
4613
4838
|
if (closed)
|
|
4614
4839
|
return;
|
|
4615
4840
|
closed = true;
|
|
4841
|
+
notifyDiscovered();
|
|
4616
4842
|
if (probing !== null)
|
|
4617
4843
|
clearInterval(probing);
|
|
4618
|
-
for (const resolve of shownWaiters.splice(0))
|
|
4619
|
-
resolve();
|
|
4620
4844
|
connectionLive();
|
|
4621
4845
|
lockListeners.clear();
|
|
4622
4846
|
readinessWatches.clear();
|
|
@@ -4628,11 +4852,13 @@ export function composeRuntime(options) {
|
|
|
4628
4852
|
conversations.close();
|
|
4629
4853
|
void recovery.retire();
|
|
4630
4854
|
roomListeners.clear();
|
|
4855
|
+
unlistedRooms.clear();
|
|
4631
4856
|
refusalListeners.clear();
|
|
4632
4857
|
storageLossListeners.clear();
|
|
4633
4858
|
statusListeners.clear();
|
|
4634
4859
|
serviceModeListeners.clear();
|
|
4635
4860
|
memberListeners.clear();
|
|
4861
|
+
authorityChangeListeners.clear();
|
|
4636
4862
|
liveQueries.close();
|
|
4637
4863
|
queueSubscriptions.clear();
|
|
4638
4864
|
roomDraftListeners.clear();
|
|
@@ -4640,6 +4866,7 @@ export function composeRuntime(options) {
|
|
|
4640
4866
|
}
|
|
4641
4867
|
async function stop() {
|
|
4642
4868
|
stopping = true;
|
|
4869
|
+
inFlight.abort();
|
|
4643
4870
|
await recovery.retire();
|
|
4644
4871
|
await Promise.all(working);
|
|
4645
4872
|
await mutations;
|
|
@@ -4653,12 +4880,12 @@ export function composeRuntime(options) {
|
|
|
4653
4880
|
observeStream();
|
|
4654
4881
|
void openLocal().then(() => {
|
|
4655
4882
|
if (capabilities.length > 0)
|
|
4656
|
-
|
|
4883
|
+
readyFromStore();
|
|
4657
4884
|
}, () => {
|
|
4658
|
-
|
|
4885
|
+
readyFromStore();
|
|
4659
4886
|
});
|
|
4660
4887
|
if (options.adapter.roomStream === undefined)
|
|
4661
|
-
void sync('unstreamed');
|
|
4888
|
+
void sync('unstreamed', null);
|
|
4662
4889
|
function planned(input, reach) {
|
|
4663
4890
|
const plan = planOf(input, roomFacts(reach), planReach(reach));
|
|
4664
4891
|
const named = plan.paths !== null &&
|
|
@@ -4700,58 +4927,56 @@ export function composeRuntime(options) {
|
|
|
4700
4927
|
stop();
|
|
4701
4928
|
};
|
|
4702
4929
|
}
|
|
4703
|
-
async function searched(input, reach
|
|
4930
|
+
async function searched(input, reach) {
|
|
4704
4931
|
const asked = searchPlanOf(input);
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
rooms: answered.coverage.rooms,
|
|
4749
|
-
conversations: named,
|
|
4750
|
-
overlay: answered.coverage.overlay,
|
|
4932
|
+
await ready;
|
|
4933
|
+
const search = options.adapter.search;
|
|
4934
|
+
if (search === undefined) {
|
|
4935
|
+
return {
|
|
4936
|
+
kind: 'undeclared',
|
|
4937
|
+
code: 'action-undeclared',
|
|
4938
|
+
fields: { name: 'search' },
|
|
4939
|
+
retryable: false,
|
|
4940
|
+
};
|
|
4941
|
+
}
|
|
4942
|
+
if (closed || refusal !== null || connectionOf() !== 'connected')
|
|
4943
|
+
return OFFLINE;
|
|
4944
|
+
const reached = reach === null ? null : new Set(reach.rooms);
|
|
4945
|
+
const named = asked.rooms === null
|
|
4946
|
+
? reached === null
|
|
4947
|
+
? null
|
|
4948
|
+
: [...reached]
|
|
4949
|
+
: asked.rooms.filter((room) => reached === null || reached.has(room));
|
|
4950
|
+
const rooms = named === null ? null : named.filter((room) => replicas.has(room)).sort();
|
|
4951
|
+
if (rooms !== null && rooms.length === 0)
|
|
4952
|
+
return { kind: 'found', documents: [] };
|
|
4953
|
+
try {
|
|
4954
|
+
await ensureAuthorization();
|
|
4955
|
+
}
|
|
4956
|
+
catch {
|
|
4957
|
+
return UNCERTAIN;
|
|
4958
|
+
}
|
|
4959
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
4960
|
+
return UNCERTAIN;
|
|
4961
|
+
journal.record('transfer search');
|
|
4962
|
+
try {
|
|
4963
|
+
const result = await search(authorization, { text: asked.text, rooms });
|
|
4964
|
+
if (result.kind === 'revoked-authorization')
|
|
4965
|
+
await refuse('revoked-authorization');
|
|
4966
|
+
journal.record(`outcome search ${result.kind}`);
|
|
4967
|
+
if (result.kind === 'rejected')
|
|
4968
|
+
return result;
|
|
4969
|
+
if (result.kind !== 'found')
|
|
4970
|
+
return failure(result);
|
|
4971
|
+
const kept = rooms === null ? null : new Set(rooms);
|
|
4972
|
+
return {
|
|
4973
|
+
kind: 'found',
|
|
4974
|
+
documents: result.documents.filter((hit) => kept === null || kept.has(hit.room)),
|
|
4751
4975
|
};
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4976
|
+
}
|
|
4977
|
+
catch {
|
|
4978
|
+
journal.record('outcome search transport-uncertainty');
|
|
4979
|
+
return UNCERTAIN;
|
|
4755
4980
|
}
|
|
4756
4981
|
}
|
|
4757
4982
|
const watch = (topic, input, deliver) => {
|
|
@@ -4803,12 +5028,13 @@ export function composeRuntime(options) {
|
|
|
4803
5028
|
};
|
|
4804
5029
|
}
|
|
4805
5030
|
if (topic === 'readiness') {
|
|
4806
|
-
return watchReadiness({ target: '', code: 'stored', space: null
|
|
5031
|
+
return watchReadiness({ target: '', code: 'stored', space: null }, null, deliver);
|
|
4807
5032
|
}
|
|
4808
5033
|
if (topic === 'rooms') {
|
|
4809
|
-
|
|
4810
|
-
|
|
5034
|
+
unlistedRooms.add(deliver);
|
|
5035
|
+
emitKnownRooms();
|
|
4811
5036
|
return () => {
|
|
5037
|
+
unlistedRooms.delete(deliver);
|
|
4812
5038
|
roomListeners.delete(deliver);
|
|
4813
5039
|
};
|
|
4814
5040
|
}
|
|
@@ -4847,6 +5073,13 @@ export function composeRuntime(options) {
|
|
|
4847
5073
|
memberListeners.delete(deliver);
|
|
4848
5074
|
};
|
|
4849
5075
|
}
|
|
5076
|
+
if (topic === 'authority-changes') {
|
|
5077
|
+
authorityChangeListeners.add(deliver);
|
|
5078
|
+
deliver(authorityChanges);
|
|
5079
|
+
return () => {
|
|
5080
|
+
authorityChangeListeners.delete(deliver);
|
|
5081
|
+
};
|
|
5082
|
+
}
|
|
4850
5083
|
if (topic === 'queue') {
|
|
4851
5084
|
const queue = { locator: input[0], notify: deliver };
|
|
4852
5085
|
queueSubscriptions.add(queue);
|
|
@@ -4862,17 +5095,27 @@ export function composeRuntime(options) {
|
|
|
4862
5095
|
return openLocal().then(() => undefined);
|
|
4863
5096
|
},
|
|
4864
5097
|
rooms() {
|
|
4865
|
-
|
|
5098
|
+
if (closed)
|
|
5099
|
+
return Promise.resolve([]);
|
|
5100
|
+
return new Promise((settle) => {
|
|
5101
|
+
const answer = (value) => {
|
|
5102
|
+
unlistedRooms.delete(answer);
|
|
5103
|
+
roomListeners.delete(answer);
|
|
5104
|
+
settle(value);
|
|
5105
|
+
};
|
|
5106
|
+
unlistedRooms.add(answer);
|
|
5107
|
+
emitKnownRooms();
|
|
5108
|
+
});
|
|
4866
5109
|
},
|
|
4867
5110
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
4868
|
-
search: (input) => searched(input, null
|
|
5111
|
+
search: (input) => searched(input, null),
|
|
4869
5112
|
types(locator) {
|
|
4870
5113
|
return {
|
|
4871
5114
|
collection: modelTypes(modelOf(locator)),
|
|
4872
5115
|
typedSelection: rooms.get(locator)?.typedSelection === undefined ||
|
|
4873
5116
|
rooms.get(locator)?.typedSelection === null
|
|
4874
5117
|
? null
|
|
4875
|
-
: modelTypes(
|
|
5118
|
+
: modelTypes(builtIn),
|
|
4876
5119
|
};
|
|
4877
5120
|
},
|
|
4878
5121
|
roomDraft: {
|
|
@@ -4951,6 +5194,8 @@ export function composeRuntime(options) {
|
|
|
4951
5194
|
part: readPart,
|
|
4952
5195
|
read: readBinaryPath,
|
|
4953
5196
|
link: linkBinaryPath,
|
|
5197
|
+
show: showBinaryPath,
|
|
5198
|
+
attach: attachBinary,
|
|
4954
5199
|
},
|
|
4955
5200
|
async call(name, input) {
|
|
4956
5201
|
await ready;
|
|
@@ -4988,7 +5233,7 @@ export function composeRuntime(options) {
|
|
|
4988
5233
|
authorization = undefined;
|
|
4989
5234
|
discoveryPaused = false;
|
|
4990
5235
|
if (options.adapter.roomStream === undefined)
|
|
4991
|
-
await sync('unstreamed');
|
|
5236
|
+
await sync('unstreamed', null);
|
|
4992
5237
|
else
|
|
4993
5238
|
await task(OPENING_LANE, ensureAuthorization);
|
|
4994
5239
|
},
|
|
@@ -5005,12 +5250,12 @@ export function composeRuntime(options) {
|
|
|
5005
5250
|
lockedRooms,
|
|
5006
5251
|
watchLocks,
|
|
5007
5252
|
located,
|
|
5253
|
+
room: (locator) => listed().find((one) => one.locator === locator),
|
|
5008
5254
|
};
|
|
5009
5255
|
return {
|
|
5010
5256
|
md,
|
|
5011
5257
|
seed,
|
|
5012
5258
|
stop,
|
|
5013
|
-
lose: loseRooms,
|
|
5014
5259
|
watch,
|
|
5015
5260
|
persisted(answer) {
|
|
5016
5261
|
if (answer === persistence)
|
|
@@ -5019,7 +5264,6 @@ export function composeRuntime(options) {
|
|
|
5019
5264
|
journal.record(`persistence ${answer}`);
|
|
5020
5265
|
queueReadiness();
|
|
5021
5266
|
},
|
|
5022
|
-
shown: screen,
|
|
5023
5267
|
within: (reach, port) => {
|
|
5024
5268
|
if (full)
|
|
5025
5269
|
void probed();
|