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