@agentshouse/mdruntime 0.1.0-c1db74d14e5fa → 0.1.0-c1ffd2bc60355
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/AGENTS.md +268 -251
- package/README.md +9 -8
- package/dist/adapter.d.ts +76 -40
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attached-model.d.ts +4 -1
- package/dist/attached-model.d.ts.map +1 -1
- package/dist/attached-model.js +13 -3
- 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 +26 -7
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +148 -78
- package/dist/chain.d.ts +11 -0
- package/dist/chain.d.ts.map +1 -0
- package/dist/chain.js +64 -0
- package/dist/change-set.d.ts +9 -16
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +11 -88
- package/dist/change.d.ts +31 -1
- package/dist/change.d.ts.map +1 -1
- package/dist/change.js +10 -1
- package/dist/chat/earlier.d.ts +10 -0
- package/dist/chat/earlier.d.ts.map +1 -0
- package/dist/chat/earlier.js +54 -0
- package/dist/chat/house.d.ts +25 -0
- package/dist/chat/house.d.ts.map +1 -0
- package/dist/chat/house.js +38 -0
- package/dist/chat/input.d.ts +3 -1
- package/dist/chat/input.d.ts.map +1 -1
- package/dist/chat/input.js +11 -18
- package/dist/chat/kit.d.ts +2 -1
- package/dist/chat/kit.d.ts.map +1 -1
- package/dist/chat/kit.js +3 -3
- package/dist/chat/list.d.ts +3 -1
- package/dist/chat/list.d.ts.map +1 -1
- package/dist/chat/list.js +47 -6
- package/dist/chat/markdown.d.ts +2 -2
- package/dist/chat/markdown.d.ts.map +1 -1
- package/dist/chat/markdown.js +80 -39
- package/dist/chat/message.d.ts +1 -0
- package/dist/chat/message.d.ts.map +1 -1
- package/dist/chat/message.js +73 -11
- package/dist/chat/messages.d.ts +15 -1
- package/dist/chat/messages.d.ts.map +1 -1
- package/dist/chat/messages.js +82 -23
- package/dist/chat/name.d.ts +1 -0
- package/dist/chat/name.d.ts.map +1 -1
- package/dist/chat/name.js +1 -1
- package/dist/chat/opened.d.ts +1 -0
- package/dist/chat/opened.d.ts.map +1 -1
- package/dist/chat/opened.js +2 -0
- package/dist/chat/overlay.d.ts +1 -0
- package/dist/chat/overlay.d.ts.map +1 -1
- package/dist/chat/overlay.js +11 -5
- package/dist/chat/parts.d.ts +1 -0
- package/dist/chat/parts.d.ts.map +1 -1
- package/dist/chat/parts.js +3 -2
- package/dist/chat/pieces.d.ts +62 -0
- package/dist/chat/pieces.d.ts.map +1 -0
- package/dist/chat/pieces.js +1110 -0
- package/dist/chat/provider.d.ts +9 -2
- package/dist/chat/provider.d.ts.map +1 -1
- package/dist/chat/provider.js +121 -106
- package/dist/chat/question.d.ts +1 -1
- package/dist/chat/question.d.ts.map +1 -1
- package/dist/chat/question.js +7 -18
- package/dist/chat/ready.d.ts +5 -1
- package/dist/chat/ready.d.ts.map +1 -1
- package/dist/chat/ready.js +7 -5
- package/dist/chat/room-file.d.ts +8 -0
- package/dist/chat/room-file.d.ts.map +1 -0
- package/dist/chat/room-file.js +72 -0
- package/dist/chat/steady.d.ts +3 -0
- package/dist/chat/steady.d.ts.map +1 -0
- package/dist/chat/steady.js +26 -0
- package/dist/chat/style.d.ts.map +1 -1
- package/dist/chat/style.js +6 -1
- package/dist/chat.d.ts +3 -2
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +2 -2
- package/dist/client.d.ts +15 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +47 -8
- package/dist/collection.d.ts +6 -19
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +30 -287
- package/dist/compile.d.ts +16 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +721 -0
- package/dist/connection.d.ts +25 -12
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +2615 -946
- package/dist/conversation.d.ts +18 -3
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +341 -107
- package/dist/digest.d.ts +7 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +113 -0
- package/dist/durability.d.ts +3 -5
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +5 -0
- package/dist/entry.d.ts +3 -1
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +18 -23
- package/dist/file.d.ts +2 -10
- package/dist/file.d.ts.map +1 -1
- package/dist/file.js +3 -120
- package/dist/fulltext.d.ts +27 -0
- package/dist/fulltext.d.ts.map +1 -0
- package/dist/fulltext.js +368 -0
- package/dist/host.d.ts +10 -8
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +95 -18
- package/dist/identity.d.ts +0 -1
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +0 -3
- package/dist/index.d.ts +15 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/live.d.ts +22 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +123 -0
- package/dist/memory.d.ts +8 -3
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +43 -25
- package/dist/messages.d.ts +28 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +90 -0
- package/dist/model.d.ts +3 -2
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +1 -1
- package/dist/name.d.ts +4 -0
- package/dist/name.d.ts.map +1 -0
- package/dist/name.js +122 -0
- package/dist/observation.d.ts +14 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +93 -3
- package/dist/opened.d.ts +22 -0
- package/dist/opened.d.ts.map +1 -0
- package/dist/opened.js +107 -0
- package/dist/outcome.d.ts +34 -4
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +25 -0
- package/dist/overlay.d.ts +3 -5
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +95 -156
- package/dist/query.d.ts +33 -2
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +48 -7
- package/dist/reach.d.ts +6 -5
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +170 -62
- package/dist/react.d.ts +3 -3
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +7 -17
- package/dist/readiness.d.ts +20 -9
- package/dist/readiness.d.ts.map +1 -1
- package/dist/readiness.js +6 -6
- package/dist/reads.d.ts +11 -0
- package/dist/reads.d.ts.map +1 -0
- package/dist/reads.js +100 -0
- package/dist/recovery.d.ts +2 -2
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +8 -7
- package/dist/refresh.d.ts +16 -15
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +112 -45
- package/dist/replica.d.ts +57 -33
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +295 -177
- package/dist/room-draft.d.ts +17 -2
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +65 -25
- package/dist/rows.d.ts +29 -29
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +113 -62
- package/dist/save.d.ts +51 -14
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +518 -180
- package/dist/search.d.ts +32 -43
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +37 -168
- package/dist/seed.d.ts +16 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +75 -0
- package/dist/shared.d.ts +8 -3
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +129 -66
- package/dist/store.d.ts +99 -88
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +335 -641
- package/dist/stored.d.ts +145 -0
- package/dist/stored.d.ts.map +1 -0
- package/dist/stored.js +567 -0
- package/dist/tables.d.ts +354 -0
- package/dist/tables.d.ts.map +1 -0
- package/dist/tables.js +624 -0
- package/dist/turn.d.ts +5 -3
- package/dist/turn.d.ts.map +1 -1
- package/dist/turn.js +51 -21
- package/dist/validation.d.ts +30 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +162 -0
- package/dist/windows.d.ts +22 -0
- package/dist/windows.d.ts.map +1 -0
- package/dist/windows.js +89 -0
- package/dist/wire.d.ts +163 -33
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +54 -32
- package/dist/words.d.ts +19 -0
- package/dist/words.d.ts.map +1 -0
- package/dist/words.js +150 -0
- package/package.json +11 -5
- package/dist/acquisition.d.ts +0 -14
- package/dist/acquisition.d.ts.map +0 -1
- package/dist/acquisition.js +0 -24
- package/dist/read-side.d.ts +0 -25
- package/dist/read-side.d.ts.map +0 -1
- package/dist/read-side.js +0 -639
package/dist/connection.js
CHANGED
|
@@ -1,34 +1,39 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
|
|
2
|
+
import { attachedModel, NO_ROOM_TYPES, roomTypesOf } from './attached-model.js';
|
|
2
3
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
3
|
-
import { observing } from './observation.js';
|
|
4
|
-
import { filterOf, identityOf, planOf, } from './query.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { observing, whole } from './observation.js';
|
|
5
|
+
import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
|
|
6
|
+
import { createLiveQueries } from './live.js';
|
|
7
|
+
import { seededResult } from './seed.js';
|
|
8
|
+
import { documentKey, roomOfKey } from './rows.js';
|
|
9
|
+
import { searchPlanOf } from './search.js';
|
|
10
|
+
import { capped, MAXIMUM_WINDOW } from './windows.js';
|
|
8
11
|
import { bootstrapRoom } from './bootstrap.js';
|
|
9
|
-
import { changeSet,
|
|
12
|
+
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
10
13
|
import { createConversationStore, } from './conversation.js';
|
|
11
|
-
import { queuedFile } from './change.js';
|
|
12
|
-
import {
|
|
14
|
+
import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
|
|
15
|
+
import { available, collectionOf, unreadable as unreadableIn, validRoot, } from './collection.js';
|
|
13
16
|
import { runtimeError } from './failure.js';
|
|
14
17
|
import { createLog } from './log.js';
|
|
15
18
|
import { binaryRecord } from './binary.js';
|
|
16
|
-
import {
|
|
19
|
+
import { openedFiles } from './opened.js';
|
|
20
|
+
import { ABSENT_TARGET, absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
|
|
17
21
|
import { createRecovery } from './recovery.js';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
+
import { doorReads } from './reads.js';
|
|
23
|
+
import { advancement } from './refresh.js';
|
|
24
|
+
import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
|
|
25
|
+
import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, sameMembership, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
|
|
26
|
+
import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
22
27
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
23
|
-
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
24
|
-
import {
|
|
25
|
-
import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
|
|
28
|
+
import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
|
|
29
|
+
import { readinessOf, } from './readiness.js';
|
|
26
30
|
import { FencedMap } from './fence.js';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
31
|
+
import { sameBinary } from './overlay.js';
|
|
32
|
+
import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
29
33
|
import { identityTime, roomDraftRevision } from './identity.js';
|
|
30
34
|
import { beneath, normalize } from './paths.js';
|
|
31
35
|
import { openRuntimeStore, } from './store.js';
|
|
36
|
+
import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
|
|
32
37
|
import { unconfirmed } from './transfer.js';
|
|
33
38
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
34
39
|
function within(root, path) {
|
|
@@ -48,19 +53,29 @@ const OPENING_LANE = 'opening';
|
|
|
48
53
|
const CONVERSATION_LANE = 'conversations';
|
|
49
54
|
const ROOM_READS = 256;
|
|
50
55
|
const ROOM_ATTEMPTS = 16;
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
const ERASE_BATCH = 512;
|
|
57
|
+
const REDERIVE_BATCH = 256;
|
|
58
|
+
const WALK_BYTES = 1 << 20;
|
|
59
|
+
const QUERY_BYTES = 1 << 20;
|
|
60
|
+
const STAGE_BYTES = 1 << 20;
|
|
61
|
+
const STAGE_FLOOR = 1 << 16;
|
|
62
|
+
// A batch aims at this much database work, so a Save or a query waits behind at most one short write.
|
|
63
|
+
const STAGE_AIM_MS = 50;
|
|
64
|
+
const PARTS_HELD = 4;
|
|
65
|
+
// Bytes still arriving are reported at most this often, well inside the 2 s progress bound, so readiness is not rebuilt for every chunk the network hands over.
|
|
66
|
+
const ARRIVAL_REPORT_MS = 250;
|
|
67
|
+
const INSTALL_STEPS = [
|
|
68
|
+
'writing',
|
|
69
|
+
'downloading',
|
|
70
|
+
'replaying',
|
|
71
|
+
'listing',
|
|
72
|
+
];
|
|
73
|
+
const CLEAN_CHANGES = 2_000;
|
|
74
|
+
// A Room that locks offline stays readable this long after the connection drops, so a laptop waking or changing network keeps working while a disconnected device locks soon after.
|
|
75
|
+
const LOCK_GRACE_MS = 120_000;
|
|
76
|
+
function operationOf(input) {
|
|
77
|
+
const operation = input?.operation_id;
|
|
78
|
+
return typeof operation === 'string' ? operation : null;
|
|
64
79
|
}
|
|
65
80
|
function abandonedOn(signal) {
|
|
66
81
|
return new Promise((resolve) => {
|
|
@@ -73,11 +88,14 @@ function abandonedOn(signal) {
|
|
|
73
88
|
function roomLane(locator) {
|
|
74
89
|
return `room ${locator}`;
|
|
75
90
|
}
|
|
91
|
+
function pendingKey(locator, id) {
|
|
92
|
+
return `${locator}\u0000${id}`;
|
|
93
|
+
}
|
|
76
94
|
export function composeRuntime(options) {
|
|
77
95
|
const localSaves = options.adapter.localSaves === true;
|
|
96
|
+
const roomTypes = roomTypesOf(options.adapter.roomTypes);
|
|
78
97
|
let authorization = options.authorization;
|
|
79
98
|
let opening;
|
|
80
|
-
let store;
|
|
81
99
|
const fences = new Map();
|
|
82
100
|
const isFenced = (locator) => fences.has(locator);
|
|
83
101
|
let fencings = 0;
|
|
@@ -87,6 +105,8 @@ export function composeRuntime(options) {
|
|
|
87
105
|
const replicas = new FencedMap(isFenced);
|
|
88
106
|
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
89
107
|
const pendings = new Map();
|
|
108
|
+
const seeds = new Map();
|
|
109
|
+
const seedWatchers = new Map();
|
|
90
110
|
const roomDrafts = new FencedMap(isFenced);
|
|
91
111
|
const presentations = new Map();
|
|
92
112
|
let limits = [];
|
|
@@ -99,6 +119,7 @@ export function composeRuntime(options) {
|
|
|
99
119
|
const liveModels = new FencedMap(isModelFenced);
|
|
100
120
|
const invalidModels = new Set();
|
|
101
121
|
const discovered = new Map();
|
|
122
|
+
const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
|
|
102
123
|
let roomsListed = false;
|
|
103
124
|
const scopedRefusals = new Map();
|
|
104
125
|
const sourceRefusals = new Map();
|
|
@@ -135,6 +156,7 @@ export function composeRuntime(options) {
|
|
|
135
156
|
let handed = false;
|
|
136
157
|
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
137
158
|
const repairing = new Set();
|
|
159
|
+
const calling = new Set();
|
|
138
160
|
const repairable = new Map();
|
|
139
161
|
let outcomeRetention = null;
|
|
140
162
|
const answering = new Map();
|
|
@@ -145,10 +167,12 @@ export function composeRuntime(options) {
|
|
|
145
167
|
let discovering = null;
|
|
146
168
|
let opened = null;
|
|
147
169
|
const owed = new Set();
|
|
170
|
+
const reloadsDue = new Map();
|
|
148
171
|
const lanes = new Map();
|
|
149
172
|
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
150
173
|
const working = new Set();
|
|
151
174
|
let mutations = Promise.resolve();
|
|
175
|
+
let gathering = null;
|
|
152
176
|
const roomListeners = new Set();
|
|
153
177
|
const refusalListeners = new Set();
|
|
154
178
|
const storageLossListeners = new Set();
|
|
@@ -156,52 +180,79 @@ export function composeRuntime(options) {
|
|
|
156
180
|
const queueSubscriptions = new Set();
|
|
157
181
|
const roomDraftListeners = new Set();
|
|
158
182
|
const presentationListeners = new Set();
|
|
159
|
-
const
|
|
183
|
+
const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover));
|
|
160
184
|
let withdrawConversations;
|
|
161
185
|
let currencyQueued = null;
|
|
162
186
|
let socketOpen = false;
|
|
187
|
+
const confirmed = new Set();
|
|
188
|
+
let lockGrace = null;
|
|
189
|
+
const lockListeners = new Set();
|
|
163
190
|
let serviceMode = 'open';
|
|
164
191
|
let haltedRefusal = false;
|
|
165
192
|
let openSignals = 0;
|
|
166
193
|
const serviceModeListeners = new Set();
|
|
167
194
|
let member = null;
|
|
168
195
|
const memberListeners = new Set();
|
|
169
|
-
const prepared = new Set();
|
|
170
|
-
const projecting = new Set();
|
|
171
196
|
const unpreparedDemand = new Set();
|
|
172
|
-
const
|
|
197
|
+
const unpreparable = new Set();
|
|
173
198
|
let preparing = false;
|
|
174
|
-
let demand = WHOLE_DEMAND;
|
|
175
|
-
let narrowing = null;
|
|
176
199
|
let targets = {};
|
|
177
200
|
let persistence = options.shell.persistence;
|
|
178
|
-
let
|
|
179
|
-
|
|
180
|
-
let
|
|
201
|
+
let full = false;
|
|
202
|
+
let probing = null;
|
|
203
|
+
let testing = null;
|
|
204
|
+
const database = {
|
|
205
|
+
async ask(request) {
|
|
206
|
+
try {
|
|
207
|
+
return await options.database.ask(request);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (refusedForSpace(error))
|
|
211
|
+
filled();
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
};
|
|
181
216
|
const readinessWatches = new Set();
|
|
182
217
|
let readinessQueued = false;
|
|
183
218
|
const catching = new Set();
|
|
184
219
|
const transfers = new FencedMap(isFenced);
|
|
220
|
+
const installs = new Map();
|
|
221
|
+
const arrivals = new Map();
|
|
222
|
+
let arrivalReported = 0;
|
|
223
|
+
let onScreen = options.shell.onScreen;
|
|
224
|
+
const shownWaiters = [];
|
|
225
|
+
let partsFree = PARTS_HELD;
|
|
226
|
+
const partWaiters = [];
|
|
227
|
+
let stageBytes = STAGE_FLOOR * 4;
|
|
228
|
+
let installWrites = Promise.resolve();
|
|
185
229
|
const conversations = createConversationStore({
|
|
186
230
|
agents: declaredAgents,
|
|
187
|
-
documents: (
|
|
231
|
+
documents: (declaration, after) => rowsQuery(planOf({
|
|
232
|
+
rooms: [declaration.locator],
|
|
233
|
+
types: [declaration.type],
|
|
234
|
+
select: {
|
|
235
|
+
fields: [
|
|
236
|
+
declaration.identity,
|
|
237
|
+
declaration.name,
|
|
238
|
+
...(declaration.retired === undefined ? [] : [declaration.retired]),
|
|
239
|
+
].map((field) => `/${escapePointer(field)}`),
|
|
240
|
+
},
|
|
241
|
+
limit: MAXIMUM_WINDOW,
|
|
242
|
+
...(after === undefined ? {} : { after }),
|
|
243
|
+
}, roomFacts())),
|
|
188
244
|
call: (name, input) => invokeAction(name, input),
|
|
189
245
|
durably,
|
|
190
246
|
stream: () => options.adapter.conversations,
|
|
191
247
|
publish: publishChangeSet,
|
|
192
|
-
online: () =>
|
|
248
|
+
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
249
|
+
full: () => full,
|
|
193
250
|
closed: () => closed,
|
|
194
251
|
unanswered: resendLater,
|
|
195
252
|
expired: (operation) => pastHorizon(operation),
|
|
196
253
|
});
|
|
197
|
-
const searchIndex = createSearchIndex({
|
|
198
|
-
room: (room) => readSide.room(room),
|
|
199
|
-
document: (room, path) => readSide.document(room, path),
|
|
200
|
-
});
|
|
201
|
-
const roomsToIndex = new Set();
|
|
202
|
-
const conversationsToIndex = new Set();
|
|
203
|
-
let indexing = false;
|
|
204
254
|
const journal = createLog();
|
|
255
|
+
const keptFiles = openedFiles(database, isFenced);
|
|
205
256
|
let notifyReady = () => { };
|
|
206
257
|
const ready = new Promise((resolve) => {
|
|
207
258
|
notifyReady = resolve;
|
|
@@ -219,16 +270,36 @@ export function composeRuntime(options) {
|
|
|
219
270
|
if (room.size === 0)
|
|
220
271
|
roomDrafts.delete(locator);
|
|
221
272
|
}
|
|
222
|
-
function
|
|
273
|
+
async function visibleOf(locator, paths) {
|
|
274
|
+
const held = replicas.get(locator);
|
|
275
|
+
if (held === undefined)
|
|
276
|
+
return undefined;
|
|
277
|
+
const local = await openLocal();
|
|
278
|
+
const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
|
|
279
|
+
return { ...slice, collection: collectionOf(slice.documents, compositionOf(held.record)) };
|
|
280
|
+
}
|
|
281
|
+
function roomDraftOf(record, visible) {
|
|
223
282
|
return structuredClone({
|
|
224
283
|
room: record.locator,
|
|
225
284
|
path: record.path,
|
|
226
285
|
revision: record.revision,
|
|
227
286
|
parts: record.parts,
|
|
228
|
-
changedUnderneath: underneath(record.parts, record.path,
|
|
287
|
+
changedUnderneath: underneath(record.parts, record.path, visible),
|
|
229
288
|
returned: record.returned,
|
|
230
289
|
});
|
|
231
290
|
}
|
|
291
|
+
async function roomDraftsOf(records) {
|
|
292
|
+
const rooms = new Map();
|
|
293
|
+
for (const record of records) {
|
|
294
|
+
const paths = rooms.get(record.locator) ?? [];
|
|
295
|
+
paths.push(record.path);
|
|
296
|
+
rooms.set(record.locator, paths);
|
|
297
|
+
}
|
|
298
|
+
const layers = new Map();
|
|
299
|
+
for (const [locator, paths] of rooms)
|
|
300
|
+
layers.set(locator, await visibleOf(locator, paths));
|
|
301
|
+
return records.map((record) => roomDraftOf(record, layers.get(record.locator)));
|
|
302
|
+
}
|
|
232
303
|
function snapshotRoomDrafts() {
|
|
233
304
|
const records = [...roomDrafts.values()].flatMap((room) => [...room.values()]);
|
|
234
305
|
records.sort((left, right) => {
|
|
@@ -238,23 +309,62 @@ export function composeRuntime(options) {
|
|
|
238
309
|
return left.path < right.path ? -1 : 1;
|
|
239
310
|
return 0;
|
|
240
311
|
});
|
|
241
|
-
return records
|
|
312
|
+
return roomDraftsOf(records);
|
|
242
313
|
}
|
|
314
|
+
let draftsShown = Promise.resolve();
|
|
243
315
|
function publishRoomDrafts(waitingOnly) {
|
|
244
316
|
if (closed)
|
|
245
317
|
return;
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
listener
|
|
252
|
-
|
|
318
|
+
const snapshot = snapshotRoomDrafts();
|
|
319
|
+
draftsShown = draftsShown.then(async () => {
|
|
320
|
+
const value = await snapshot.catch(() => null);
|
|
321
|
+
if (value === null || closed)
|
|
322
|
+
return;
|
|
323
|
+
for (const listener of roomDraftListeners) {
|
|
324
|
+
if (waitingOnly && listener.ready)
|
|
325
|
+
continue;
|
|
326
|
+
listener.ready = true;
|
|
327
|
+
listener.notify(value);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
253
330
|
}
|
|
254
331
|
function presentationOf(app) {
|
|
255
332
|
const value = presentations.get(app);
|
|
256
333
|
return value === undefined ? null : structuredClone(value);
|
|
257
334
|
}
|
|
335
|
+
async function writtenRoomDraft(room, path, revision, input, next) {
|
|
336
|
+
const parts = readParts(input);
|
|
337
|
+
if (path === '' || normalize(path) !== path)
|
|
338
|
+
throw runtimeError('room-draft-malformed');
|
|
339
|
+
const local = await openLocal();
|
|
340
|
+
const current = roomDrafts.get(room)?.get(path);
|
|
341
|
+
if ((current?.revision ?? null) !== revision) {
|
|
342
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
343
|
+
}
|
|
344
|
+
const file = parts.file === undefined
|
|
345
|
+
? undefined
|
|
346
|
+
: await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
|
|
347
|
+
const record = {
|
|
348
|
+
locator: room,
|
|
349
|
+
path,
|
|
350
|
+
revision: next,
|
|
351
|
+
parts: held(current?.parts, parts, file),
|
|
352
|
+
returned: current?.returned ?? null,
|
|
353
|
+
};
|
|
354
|
+
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
355
|
+
? []
|
|
356
|
+
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
357
|
+
rememberRoomDraft(record);
|
|
358
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
359
|
+
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
360
|
+
}
|
|
361
|
+
async function keptPresentation(app, next) {
|
|
362
|
+
const local = await openLocal();
|
|
363
|
+
const stored = next(presentations.get(app));
|
|
364
|
+
await local.present({ app, value: stored });
|
|
365
|
+
presentations.set(app, stored);
|
|
366
|
+
publishChangeSet(changeSet('', { presentation: app }));
|
|
367
|
+
}
|
|
258
368
|
function emitPresentation(app) {
|
|
259
369
|
if (closed)
|
|
260
370
|
return;
|
|
@@ -303,21 +413,69 @@ export function composeRuntime(options) {
|
|
|
303
413
|
return { attentionRequired, reset };
|
|
304
414
|
}
|
|
305
415
|
function documentsIn(locator) {
|
|
306
|
-
|
|
307
|
-
if (replica === undefined)
|
|
308
|
-
return 0;
|
|
309
|
-
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
310
|
-
const weighed = outsideOf(roots);
|
|
311
|
-
return (tally(replica.visible.documents, weighed.documents) +
|
|
312
|
-
tally(replica.visible.binaries, weighed.binaries));
|
|
416
|
+
return replicas.get(locator)?.count ?? 0;
|
|
313
417
|
}
|
|
314
418
|
function listed() {
|
|
315
419
|
return structuredClone([...rooms.values()].map((room) => {
|
|
316
420
|
const documents = documentsIn(room.locator);
|
|
317
421
|
counted.set(room.locator, documents);
|
|
318
|
-
return
|
|
422
|
+
return locked(room.locator)
|
|
423
|
+
? {
|
|
424
|
+
...room,
|
|
425
|
+
model: modelTypes(NO_MODEL),
|
|
426
|
+
typedSelection: null,
|
|
427
|
+
documents: 0,
|
|
428
|
+
locked: true,
|
|
429
|
+
}
|
|
430
|
+
: { ...room, documents, locked: false };
|
|
319
431
|
}));
|
|
320
432
|
}
|
|
433
|
+
function locked(locator) {
|
|
434
|
+
return rooms.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
|
|
435
|
+
}
|
|
436
|
+
function lockedRooms() {
|
|
437
|
+
return [...rooms.keys()].filter(locked).sort();
|
|
438
|
+
}
|
|
439
|
+
function relocked(before) {
|
|
440
|
+
const after = lockedRooms();
|
|
441
|
+
if (JSON.stringify(after) === JSON.stringify(before))
|
|
442
|
+
return;
|
|
443
|
+
journal.record(`locked ${after.length}`);
|
|
444
|
+
emitRooms();
|
|
445
|
+
for (const notify of [...lockListeners])
|
|
446
|
+
notify();
|
|
447
|
+
}
|
|
448
|
+
function confirmLocks(listed) {
|
|
449
|
+
if (!socketOpen)
|
|
450
|
+
return;
|
|
451
|
+
for (const room of listed) {
|
|
452
|
+
if (room.replica.admission === 'admitted')
|
|
453
|
+
confirmed.add(room.locator);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function connectionLost() {
|
|
457
|
+
if (lockGrace !== null || closed)
|
|
458
|
+
return;
|
|
459
|
+
lockGrace = setTimeout(() => {
|
|
460
|
+
lockGrace = null;
|
|
461
|
+
if (socketOpen || closed)
|
|
462
|
+
return;
|
|
463
|
+
const before = lockedRooms();
|
|
464
|
+
confirmed.clear();
|
|
465
|
+
relocked(before);
|
|
466
|
+
}, LOCK_GRACE_MS);
|
|
467
|
+
}
|
|
468
|
+
function connectionLive() {
|
|
469
|
+
if (lockGrace !== null)
|
|
470
|
+
clearTimeout(lockGrace);
|
|
471
|
+
lockGrace = null;
|
|
472
|
+
}
|
|
473
|
+
function watchLocks(notify) {
|
|
474
|
+
lockListeners.add(notify);
|
|
475
|
+
return () => {
|
|
476
|
+
lockListeners.delete(notify);
|
|
477
|
+
};
|
|
478
|
+
}
|
|
321
479
|
function roomFacts(reach = null) {
|
|
322
480
|
const held = [...rooms.values()]
|
|
323
481
|
.filter((room) => holds(reach, room.locator))
|
|
@@ -336,7 +494,7 @@ export function composeRuntime(options) {
|
|
|
336
494
|
return { listed: roomsListed, rooms: held };
|
|
337
495
|
}
|
|
338
496
|
function emitRooms() {
|
|
339
|
-
|
|
497
|
+
liveQueries.recover();
|
|
340
498
|
queueReadiness();
|
|
341
499
|
const value = listed();
|
|
342
500
|
for (const listener of roomListeners)
|
|
@@ -366,7 +524,7 @@ export function composeRuntime(options) {
|
|
|
366
524
|
if (closed)
|
|
367
525
|
return;
|
|
368
526
|
queueReadiness();
|
|
369
|
-
const next = status.state === 'synced' &&
|
|
527
|
+
const next = status.state === 'synced' && !conversations.current()
|
|
370
528
|
? { ...status, state: 'syncing' }
|
|
371
529
|
: status;
|
|
372
530
|
if (same(shown, next))
|
|
@@ -375,17 +533,17 @@ export function composeRuntime(options) {
|
|
|
375
533
|
for (const listener of statusListeners)
|
|
376
534
|
listener(shown);
|
|
377
535
|
}
|
|
378
|
-
function entryOf(batch) {
|
|
536
|
+
function entryOf(locator, batch) {
|
|
379
537
|
if (localSaves)
|
|
380
538
|
return Object.freeze({ batch, state: 'local' });
|
|
381
|
-
const state = pendings.get(batch.id);
|
|
539
|
+
const state = pendings.get(pendingKey(locator, batch.id));
|
|
382
540
|
if (state?.accepted !== undefined) {
|
|
383
541
|
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
384
542
|
}
|
|
385
543
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
386
544
|
}
|
|
387
545
|
function entriesOf(locator) {
|
|
388
|
-
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
546
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map((batch) => entryOf(locator, batch));
|
|
389
547
|
}
|
|
390
548
|
function emitQueue(locator) {
|
|
391
549
|
const entries = entriesOf(locator);
|
|
@@ -395,62 +553,39 @@ export function composeRuntime(options) {
|
|
|
395
553
|
}
|
|
396
554
|
}
|
|
397
555
|
function applyChangeSet(set) {
|
|
398
|
-
|
|
399
|
-
...set,
|
|
400
|
-
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
401
|
-
};
|
|
402
|
-
readSide.apply(held);
|
|
403
|
-
searchIndex.changed(held);
|
|
556
|
+
liveQueries.touched(set);
|
|
404
557
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const held = { text: record.documents, binaries: record.binaries };
|
|
411
|
-
const composition = compositionOf(record);
|
|
412
|
-
if (referencesIndexed(held, composition))
|
|
413
|
-
return;
|
|
414
|
-
await indexReferrers(held, composition);
|
|
415
|
-
}
|
|
558
|
+
function unpublished(error) {
|
|
559
|
+
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
560
|
+
}
|
|
561
|
+
function indexed(locator) {
|
|
562
|
+
return replicas.get(locator)?.indexed === true;
|
|
416
563
|
}
|
|
417
564
|
async function prepareReplica(locator) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
return;
|
|
422
|
-
const visible = replica.visible;
|
|
423
|
-
const paths = [
|
|
424
|
-
...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
|
|
425
|
-
];
|
|
426
|
-
const entered = [];
|
|
427
|
-
projecting.add(locator);
|
|
428
|
-
try {
|
|
429
|
-
await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
430
|
-
const current = replicas.get(locator);
|
|
431
|
-
if (current === undefined || closed)
|
|
432
|
-
return;
|
|
433
|
-
const set = entering(locator, current, chunk);
|
|
434
|
-
readSide.apply(set);
|
|
435
|
-
entered.push(set);
|
|
436
|
-
});
|
|
437
|
-
if (!replicas.has(locator) || closed)
|
|
565
|
+
for (;;) {
|
|
566
|
+
const held = replicas.get(locator);
|
|
567
|
+
if (held === undefined || closed || held.indexed)
|
|
438
568
|
return;
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
569
|
+
const key = held.record.key;
|
|
570
|
+
const done = await exclusive(async () => (await openLocal()).rederive(key, REDERIVE_BATCH));
|
|
571
|
+
const now = replicas.get(locator);
|
|
572
|
+
if (now?.record.key !== key)
|
|
573
|
+
continue;
|
|
574
|
+
if (!done)
|
|
575
|
+
continue;
|
|
576
|
+
replicas.set(locator, { ...now, indexed: true });
|
|
577
|
+
publishChangeSet(changeSet(locator, { documents: null }));
|
|
578
|
+
liveQueries.recover();
|
|
579
|
+
queueReadiness();
|
|
580
|
+
return;
|
|
443
581
|
}
|
|
444
|
-
conversations.changed(mergeChangeSets(entered));
|
|
445
|
-
readSide.recover();
|
|
446
|
-
queueReadiness();
|
|
447
582
|
}
|
|
448
583
|
function unprepared() {
|
|
449
|
-
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !
|
|
584
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
|
|
450
585
|
}
|
|
451
|
-
function
|
|
452
|
-
|
|
453
|
-
journal.record(`
|
|
586
|
+
function unpreparedBy(locator, error) {
|
|
587
|
+
unpreparable.add(locator);
|
|
588
|
+
journal.record(`preparation failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
454
589
|
queueReadiness();
|
|
455
590
|
}
|
|
456
591
|
function prepare(locators = []) {
|
|
@@ -466,7 +601,7 @@ export function composeRuntime(options) {
|
|
|
466
601
|
if (closed)
|
|
467
602
|
return;
|
|
468
603
|
unpreparedDemand.delete(waiting);
|
|
469
|
-
await prepareReplica(waiting).catch((error) =>
|
|
604
|
+
await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
|
|
470
605
|
}
|
|
471
606
|
}
|
|
472
607
|
finally {
|
|
@@ -474,17 +609,22 @@ export function composeRuntime(options) {
|
|
|
474
609
|
}
|
|
475
610
|
})();
|
|
476
611
|
}
|
|
612
|
+
function roomsOfReach(reach) {
|
|
613
|
+
return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
614
|
+
}
|
|
477
615
|
function reachedRooms(plan) {
|
|
478
|
-
const
|
|
479
|
-
const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
616
|
+
const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
|
|
480
617
|
const named = plan.rooms ??
|
|
481
618
|
(reached === null
|
|
482
619
|
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
483
620
|
: [...reached]);
|
|
484
|
-
return [...new Set(named)]
|
|
621
|
+
return [...new Set(named)]
|
|
622
|
+
.filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
|
|
623
|
+
.sort();
|
|
485
624
|
}
|
|
486
625
|
function awaited(locator) {
|
|
487
|
-
return
|
|
626
|
+
return (!roomsListed ||
|
|
627
|
+
(rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
|
|
488
628
|
}
|
|
489
629
|
function covered(plan) {
|
|
490
630
|
const locators = reachedRooms(plan);
|
|
@@ -494,6 +634,7 @@ export function composeRuntime(options) {
|
|
|
494
634
|
const pending = [];
|
|
495
635
|
const coverage = [];
|
|
496
636
|
const overlay = [];
|
|
637
|
+
const read = [];
|
|
497
638
|
for (const locator of locators) {
|
|
498
639
|
const replica = replicas.get(locator);
|
|
499
640
|
if (replica === undefined) {
|
|
@@ -501,21 +642,22 @@ export function composeRuntime(options) {
|
|
|
501
642
|
pending.push({ room: locator, awaits: 'acquisition' });
|
|
502
643
|
continue;
|
|
503
644
|
}
|
|
504
|
-
if (!
|
|
645
|
+
if (!replica.indexed) {
|
|
505
646
|
pending.push({ room: locator, awaits: 'preparation' });
|
|
506
647
|
continue;
|
|
507
648
|
}
|
|
508
649
|
coverage.push({
|
|
509
650
|
room: locator,
|
|
510
|
-
generation: replica.
|
|
651
|
+
generation: replica.record.transfer,
|
|
511
652
|
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
512
653
|
});
|
|
654
|
+
read.push({ locator, key: replica.record.key });
|
|
513
655
|
for (const batch of replica.queue)
|
|
514
656
|
overlay.push(batch.id);
|
|
515
657
|
}
|
|
516
658
|
if (pending.length > 0)
|
|
517
659
|
return { pending };
|
|
518
|
-
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
660
|
+
return { coverage: { rooms: coverage, indexes: [], overlay }, rooms: read };
|
|
519
661
|
}
|
|
520
662
|
function awaitPreparation(cover) {
|
|
521
663
|
if (!('pending' in cover))
|
|
@@ -524,66 +666,16 @@ export function composeRuntime(options) {
|
|
|
524
666
|
if (waiting.length > 0)
|
|
525
667
|
prepare(waiting.map(({ room }) => room));
|
|
526
668
|
}
|
|
527
|
-
function
|
|
528
|
-
|
|
529
|
-
(replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
|
|
530
|
-
queuesWithoutReplica.has(room.locator));
|
|
531
|
-
}
|
|
532
|
-
function acquiredLocators() {
|
|
533
|
-
return [...discovered.values()]
|
|
534
|
-
.filter(acquires)
|
|
535
|
-
.map((room) => room.locator)
|
|
536
|
-
.sort();
|
|
537
|
-
}
|
|
538
|
-
function heldDemand(acquired) {
|
|
539
|
-
if (demand.whole)
|
|
540
|
-
return null;
|
|
541
|
-
return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
|
|
542
|
-
}
|
|
543
|
-
function applyDemand(next) {
|
|
544
|
-
const before = acquiredLocators();
|
|
545
|
-
const conversed = demand.conversations;
|
|
546
|
-
demand = next;
|
|
547
|
-
const after = acquiredLocators();
|
|
548
|
-
queueReadiness();
|
|
549
|
-
if (!conversed && next.conversations)
|
|
550
|
-
scheduleCurrency();
|
|
551
|
-
if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
|
|
552
|
-
return;
|
|
553
|
-
journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
|
|
554
|
-
if (options.adapter.roomStream === undefined)
|
|
555
|
-
void sync('unstreamed');
|
|
556
|
-
else if (socketOpen)
|
|
557
|
-
options.adapter.roomStream.reopen();
|
|
558
|
-
}
|
|
559
|
-
function acquire(next) {
|
|
560
|
-
if (closed)
|
|
561
|
-
return;
|
|
562
|
-
if (narrowing !== null)
|
|
563
|
-
clearTimeout(narrowing);
|
|
564
|
-
narrowing = null;
|
|
565
|
-
const widening = wider(next, demand);
|
|
566
|
-
if (!widening && !narrower(next, demand))
|
|
567
|
-
return;
|
|
568
|
-
if (widening || !roomsListed) {
|
|
569
|
-
applyDemand(next);
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
// A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
|
|
573
|
-
narrowing = setTimeout(() => {
|
|
574
|
-
narrowing = null;
|
|
575
|
-
applyDemand(next);
|
|
576
|
-
}, 5_000);
|
|
577
|
-
}
|
|
578
|
-
function requiredRooms(reach) {
|
|
669
|
+
function requiredRooms(given) {
|
|
670
|
+
const reach = planReach(given);
|
|
579
671
|
if (reach === null)
|
|
580
672
|
return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
|
|
581
|
-
const named = [...
|
|
673
|
+
const named = [...roomsOfReach(reach)].sort();
|
|
582
674
|
if (roomsListed)
|
|
583
675
|
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
584
676
|
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
585
677
|
}
|
|
586
|
-
function roomState(locator
|
|
678
|
+
function roomState(locator) {
|
|
587
679
|
if (sourceRefusals.has(locator))
|
|
588
680
|
return 'refused';
|
|
589
681
|
const listedRoom = discovered.get(locator);
|
|
@@ -594,27 +686,17 @@ export function composeRuntime(options) {
|
|
|
594
686
|
return 'unadmitted';
|
|
595
687
|
if (!replicas.has(locator))
|
|
596
688
|
return 'acquiring';
|
|
597
|
-
if (!
|
|
689
|
+
if (!indexed(locator))
|
|
598
690
|
return 'preparing';
|
|
599
|
-
if (search && !searchIndex.rooms().has(locator))
|
|
600
|
-
return 'indexing';
|
|
601
691
|
return 'complete';
|
|
602
692
|
}
|
|
603
|
-
function conversationCoverage(reach
|
|
693
|
+
function conversationCoverage(reach) {
|
|
604
694
|
const declared = reach === null || reach.conversations;
|
|
605
695
|
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
606
696
|
return 'undeclared';
|
|
607
697
|
if (!conversations.known() || !conversations.current())
|
|
608
698
|
return 'acquiring';
|
|
609
|
-
|
|
610
|
-
return 'complete';
|
|
611
|
-
const unindexed = conversations
|
|
612
|
-
.searchable()
|
|
613
|
-
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
614
|
-
if (unindexed.length === 0)
|
|
615
|
-
return 'complete';
|
|
616
|
-
index([], unindexed);
|
|
617
|
-
return 'indexing';
|
|
699
|
+
return 'complete';
|
|
618
700
|
}
|
|
619
701
|
function connectionOf() {
|
|
620
702
|
if (socketOpen)
|
|
@@ -634,6 +716,7 @@ export function composeRuntime(options) {
|
|
|
634
716
|
return [
|
|
635
717
|
{
|
|
636
718
|
room,
|
|
719
|
+
name: discovered.get(room)?.name ?? room,
|
|
637
720
|
file: typeof file === 'string' ? file : '',
|
|
638
721
|
rule: held.code,
|
|
639
722
|
location: typeof location === 'string' ? location : null,
|
|
@@ -641,48 +724,83 @@ export function composeRuntime(options) {
|
|
|
641
724
|
];
|
|
642
725
|
});
|
|
643
726
|
}
|
|
727
|
+
function installationOf(coverage) {
|
|
728
|
+
const rooms = { done: 0, total: 0 };
|
|
729
|
+
const parts = { done: 0, total: 0 };
|
|
730
|
+
const bytes = { done: 0, total: 0 };
|
|
731
|
+
const received = { done: 0, total: 0 };
|
|
732
|
+
const steps = new Set();
|
|
733
|
+
let waiting = false;
|
|
734
|
+
for (const { room, state } of coverage) {
|
|
735
|
+
if (state === 'unadmitted')
|
|
736
|
+
continue;
|
|
737
|
+
rooms.total += 1;
|
|
738
|
+
if (state === 'complete')
|
|
739
|
+
rooms.done += 1;
|
|
740
|
+
const transfer = transfers.get(room);
|
|
741
|
+
const held = installs.get(room);
|
|
742
|
+
const inventory = transfer?.inventory ?? replicas.get(room)?.record.inventory ?? null;
|
|
743
|
+
if (inventory !== null) {
|
|
744
|
+
const done = transfer?.done ?? inventory;
|
|
745
|
+
const share = held?.share ?? null;
|
|
746
|
+
parts.total += inventory.parts;
|
|
747
|
+
bytes.total += inventory.bytes;
|
|
748
|
+
parts.done += done.parts;
|
|
749
|
+
bytes.done += done.bytes;
|
|
750
|
+
if (share !== null && share.entries > 0) {
|
|
751
|
+
bytes.done += Math.floor((share.bytes * share.written) / share.entries);
|
|
752
|
+
}
|
|
753
|
+
received.total += inventory.bytes;
|
|
754
|
+
received.done += done.bytes + (share?.bytes ?? 0);
|
|
755
|
+
for (const arrived of arrivals.get(room)?.values() ?? [])
|
|
756
|
+
received.done += arrived;
|
|
757
|
+
}
|
|
758
|
+
const installing = held !== undefined && state !== 'complete';
|
|
759
|
+
if (installing)
|
|
760
|
+
steps.add(held.step);
|
|
761
|
+
waiting ||= installing || state === 'acquiring';
|
|
762
|
+
}
|
|
763
|
+
return {
|
|
764
|
+
step: INSTALL_STEPS.find((step) => steps.has(step)) ?? null,
|
|
765
|
+
paused: waiting && !onScreen ? 'hidden' : null,
|
|
766
|
+
rooms,
|
|
767
|
+
parts,
|
|
768
|
+
bytes,
|
|
769
|
+
received,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
644
772
|
function readinessFor(port, reach) {
|
|
645
773
|
const required = requiredRooms(reach);
|
|
646
|
-
const
|
|
647
|
-
const
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
.filter((room) => room.state === state && !unindexable.has(room.room))
|
|
774
|
+
const search = port.search === 'required';
|
|
775
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
|
|
776
|
+
const preparing = coverage
|
|
777
|
+
.filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
|
|
651
778
|
.map(({ room }) => room);
|
|
652
|
-
const preparing = building('preparing');
|
|
653
779
|
if (preparing.length > 0)
|
|
654
780
|
prepare(preparing);
|
|
655
|
-
const indexing = building('indexing');
|
|
656
|
-
if (indexing.length > 0)
|
|
657
|
-
index(indexing, []);
|
|
658
781
|
return readinessOf({
|
|
659
782
|
code: port.code,
|
|
660
783
|
coverage: {
|
|
661
784
|
listed: required.listed,
|
|
662
785
|
rooms: coverage,
|
|
663
|
-
conversations: conversationCoverage(reach
|
|
786
|
+
conversations: conversationCoverage(reach),
|
|
664
787
|
search: search ? 'required' : 'unused',
|
|
665
788
|
},
|
|
666
789
|
persistence,
|
|
667
790
|
connection: connectionOf(),
|
|
668
791
|
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
? capacity
|
|
672
|
-
: port.capacity,
|
|
792
|
+
space: full ? 'full' : port.space,
|
|
793
|
+
installation: installationOf(coverage),
|
|
673
794
|
refused: refusedIn(required.rooms),
|
|
674
795
|
lost: storageLoss !== null,
|
|
675
|
-
established: target?.established === true,
|
|
796
|
+
established: targets[port.target]?.established === true,
|
|
676
797
|
});
|
|
677
798
|
}
|
|
678
|
-
function
|
|
679
|
-
|
|
680
|
-
const next = { ...held, ...change };
|
|
681
|
-
if (next.established === held.established && next.search === held.search)
|
|
682
|
-
return;
|
|
683
|
-
targets = { ...targets, [name]: next };
|
|
799
|
+
function establish(name) {
|
|
800
|
+
targets = { ...targets, [name]: { established: true } };
|
|
684
801
|
queueReadiness();
|
|
685
|
-
|
|
802
|
+
probeEachMinute();
|
|
803
|
+
void durably((local) => local.fact('targets', targets)).catch(() => {
|
|
686
804
|
journal.record(`target record failed ${name}`);
|
|
687
805
|
});
|
|
688
806
|
}
|
|
@@ -691,7 +809,7 @@ export function composeRuntime(options) {
|
|
|
691
809
|
return;
|
|
692
810
|
const readiness = readinessFor(watch.port, watch.reach);
|
|
693
811
|
if (readiness.ready && targets[watch.port.target]?.established !== true) {
|
|
694
|
-
|
|
812
|
+
establish(watch.port.target);
|
|
695
813
|
}
|
|
696
814
|
const shown = JSON.stringify(readiness);
|
|
697
815
|
if (shown === watch.last)
|
|
@@ -719,157 +837,170 @@ export function composeRuntime(options) {
|
|
|
719
837
|
readinessWatches.delete(watch);
|
|
720
838
|
};
|
|
721
839
|
}
|
|
722
|
-
function
|
|
723
|
-
if (
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
let bytes = 0;
|
|
728
|
-
for (const [path, text] of Object.entries(layer.documents))
|
|
729
|
-
bytes += (path.length + text.length) * 2;
|
|
730
|
-
return bytes;
|
|
731
|
-
}
|
|
732
|
-
function grownBy(next, previous) {
|
|
733
|
-
if (previous === undefined)
|
|
734
|
-
return layerBytes(next);
|
|
735
|
-
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
736
|
-
return layerBytes({ ...next, documents });
|
|
737
|
-
}
|
|
738
|
-
function exceeded(error) {
|
|
739
|
-
return (typeof error === 'object' &&
|
|
740
|
-
error !== null &&
|
|
741
|
-
error.name === 'QuotaExceededError');
|
|
742
|
-
}
|
|
743
|
-
async function estimated() {
|
|
744
|
-
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
745
|
-
return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
|
|
746
|
-
}
|
|
747
|
-
function pause(locator, cause) {
|
|
748
|
-
paused.add(locator);
|
|
749
|
-
capacity = cause;
|
|
750
|
-
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
840
|
+
function filled() {
|
|
841
|
+
if (full || closed)
|
|
842
|
+
return;
|
|
843
|
+
full = true;
|
|
844
|
+
journal.record('space full');
|
|
751
845
|
queueReadiness();
|
|
752
|
-
|
|
846
|
+
probeEachMinute();
|
|
847
|
+
}
|
|
848
|
+
function installedTarget() {
|
|
849
|
+
return Object.values(targets).some((target) => target.established);
|
|
850
|
+
}
|
|
851
|
+
function probeEachMinute() {
|
|
852
|
+
if (!full || probing !== null || closed || !installedTarget())
|
|
753
853
|
return;
|
|
754
|
-
// A
|
|
755
|
-
|
|
756
|
-
void
|
|
854
|
+
// 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.
|
|
855
|
+
probing = setInterval(() => {
|
|
856
|
+
void probed();
|
|
757
857
|
}, 60_000);
|
|
758
858
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
859
|
+
function probed() {
|
|
860
|
+
testing ??= database.ask({ kind: 'space.probe' }).then(() => {
|
|
861
|
+
testing = null;
|
|
862
|
+
spaceFreed();
|
|
863
|
+
return true;
|
|
864
|
+
}, () => {
|
|
865
|
+
testing = null;
|
|
866
|
+
return false;
|
|
867
|
+
});
|
|
868
|
+
return testing;
|
|
769
869
|
}
|
|
770
|
-
function
|
|
771
|
-
if (
|
|
772
|
-
clearInterval(rechecking);
|
|
773
|
-
rechecking = null;
|
|
774
|
-
if (paused.size === 0 && capacity === null)
|
|
870
|
+
function spaceFreed() {
|
|
871
|
+
if (!full)
|
|
775
872
|
return;
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
873
|
+
full = false;
|
|
874
|
+
if (probing !== null)
|
|
875
|
+
clearInterval(probing);
|
|
876
|
+
probing = null;
|
|
877
|
+
unpreparable.clear();
|
|
878
|
+
journal.record('space freed');
|
|
779
879
|
queueReadiness();
|
|
780
880
|
if (!closed && authorization !== undefined)
|
|
781
881
|
void sync('unstreamed');
|
|
782
882
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
883
|
+
function seedRoomHeld(room, version) {
|
|
884
|
+
return indexed(room) && reachedBy(room, version);
|
|
885
|
+
}
|
|
886
|
+
function seedHeld(at) {
|
|
887
|
+
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
888
|
+
}
|
|
889
|
+
function seedApplies(plan) {
|
|
890
|
+
const seed = seeds.get(plan.key);
|
|
891
|
+
if (seed === undefined)
|
|
892
|
+
return null;
|
|
893
|
+
if (!seedHeld(seed.at))
|
|
894
|
+
return seed;
|
|
895
|
+
seeds.delete(plan.key);
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
function seedBehind(plan, cover) {
|
|
899
|
+
const seed = seedApplies(plan);
|
|
900
|
+
if (seed === null || 'pending' in cover)
|
|
901
|
+
return cover;
|
|
902
|
+
return {
|
|
903
|
+
pending: Object.entries(seed.at)
|
|
904
|
+
.filter(([room, version]) => !seedRoomHeld(room, version))
|
|
905
|
+
.map(([room]) => ({ room, awaits: 'acquisition' })),
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
function seed(entry) {
|
|
909
|
+
if (seedHeld(entry.at))
|
|
910
|
+
return;
|
|
911
|
+
for (const { query, rows } of entry.queries) {
|
|
912
|
+
const key = queryKey(query);
|
|
913
|
+
seeds.set(key, { rows, at: entry.at });
|
|
914
|
+
for (const shown of seedWatchers.get(key) ?? [])
|
|
915
|
+
shown();
|
|
791
916
|
}
|
|
792
|
-
if (unreferenced.length > 0)
|
|
793
|
-
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
794
|
-
return unreferenced.length > 0;
|
|
795
917
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
return false;
|
|
799
|
-
let available = await estimated().catch(() => null);
|
|
800
|
-
if (available === null || available >= needed)
|
|
801
|
-
return true;
|
|
802
|
-
if (await reclaimed(local))
|
|
803
|
-
available = await estimated().catch(() => null);
|
|
804
|
-
if (available === null || available >= needed)
|
|
805
|
-
return true;
|
|
806
|
-
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
807
|
-
return false;
|
|
918
|
+
function sameCover(left, right) {
|
|
919
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
808
920
|
}
|
|
809
|
-
async function
|
|
810
|
-
|
|
811
|
-
|
|
921
|
+
async function coveredRows(plan, cover) {
|
|
922
|
+
for (;;) {
|
|
923
|
+
const before = cover();
|
|
924
|
+
if ('pending' in before)
|
|
925
|
+
return { pending: before };
|
|
926
|
+
const local = await openLocal();
|
|
927
|
+
const keys = before.rooms.map(({ key }) => key);
|
|
928
|
+
const changes = local.changes(keys);
|
|
929
|
+
const current = () => local.changes(keys) === changes;
|
|
930
|
+
const rows = await windowRows(local, plan, before.rooms, current);
|
|
931
|
+
const after = cover();
|
|
932
|
+
if (rows !== null && sameCover(before, after))
|
|
933
|
+
return { rows, cover: before, current };
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
async function windowRows(local, plan, rooms, unchanged) {
|
|
937
|
+
const first = await local.query(plan, rooms, QUERY_BYTES);
|
|
938
|
+
if (first.more === undefined || !('documents' in first)) {
|
|
939
|
+
const { more: _more, ...rows } = first;
|
|
940
|
+
return rows;
|
|
941
|
+
}
|
|
942
|
+
const documents = [...first.documents];
|
|
943
|
+
for (let more = first.more; more !== undefined;) {
|
|
944
|
+
if (!unchanged())
|
|
945
|
+
return null;
|
|
946
|
+
const page = await local.query({
|
|
947
|
+
...plan,
|
|
948
|
+
after: { text: more, values: JSON.parse(more) },
|
|
949
|
+
limit: plan.limit === null ? null : plan.limit - documents.length,
|
|
950
|
+
total: false,
|
|
951
|
+
}, rooms, QUERY_BYTES);
|
|
952
|
+
if (!('documents' in page))
|
|
953
|
+
return null;
|
|
954
|
+
documents.push(...page.documents);
|
|
955
|
+
more = page.more;
|
|
956
|
+
if (more === undefined) {
|
|
957
|
+
return {
|
|
958
|
+
documents,
|
|
959
|
+
...(page.next === undefined ? {} : { next: page.next }),
|
|
960
|
+
...(first.total === undefined ? {} : { total: first.total }),
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
function keyedOf(rows, coverage) {
|
|
967
|
+
if ('groups' in rows) {
|
|
968
|
+
return {
|
|
969
|
+
result: { ...rows, coverage },
|
|
970
|
+
keys: rows.groups.map((group) => JSON.stringify(group.key)),
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
result: { ...rows, coverage },
|
|
975
|
+
keys: rows.documents.map((document) => documentKey(document.room, document.path)),
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
async function answeredPlan(plan, cover) {
|
|
979
|
+
const answered = await coveredRows(plan, cover);
|
|
980
|
+
if ('pending' in answered) {
|
|
981
|
+
const pending = answered.pending;
|
|
982
|
+
return { result: pending, keys: [], current: () => true };
|
|
983
|
+
}
|
|
984
|
+
const held = answered.cover;
|
|
985
|
+
return { ...keyedOf(answered.rows, held.coverage), current: answered.current };
|
|
812
986
|
}
|
|
813
987
|
async function rowsQuery(plan) {
|
|
814
988
|
const before = covered(plan);
|
|
815
989
|
awaitPreparation(before);
|
|
990
|
+
const seeded = seedApplies(plan);
|
|
991
|
+
if (seeded !== null)
|
|
992
|
+
return seededResult(seeded).result;
|
|
816
993
|
if ('pending' in before)
|
|
817
994
|
return before;
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
|
|
995
|
+
const answered = await coveredRows(plan, () => {
|
|
996
|
+
const cover = covered(plan);
|
|
997
|
+
awaitPreparation(cover);
|
|
821
998
|
return cover;
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
searchIndex.indexMessages(conversation, [], false);
|
|
828
|
-
let after = null;
|
|
829
|
-
for (;;) {
|
|
830
|
-
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
831
|
-
if (closed)
|
|
832
|
-
return;
|
|
833
|
-
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
834
|
-
if (batch.length === 0)
|
|
835
|
-
return;
|
|
836
|
-
after = batch[batch.length - 1].identity;
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
function indexNext() {
|
|
840
|
-
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
841
|
-
if (room !== undefined) {
|
|
842
|
-
roomsToIndex.delete(room);
|
|
843
|
-
return () => prepared.has(room)
|
|
844
|
-
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
845
|
-
: undefined;
|
|
846
|
-
}
|
|
847
|
-
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
848
|
-
if (conversation === undefined)
|
|
849
|
-
return null;
|
|
850
|
-
conversationsToIndex.delete(conversation);
|
|
851
|
-
return () => indexConversation(conversation);
|
|
852
|
-
}
|
|
853
|
-
function index(rooms, named) {
|
|
854
|
-
for (const room of rooms)
|
|
855
|
-
roomsToIndex.add(room);
|
|
856
|
-
for (const conversation of named)
|
|
857
|
-
conversationsToIndex.add(conversation);
|
|
858
|
-
if (indexing)
|
|
859
|
-
return;
|
|
860
|
-
indexing = true;
|
|
861
|
-
void (async () => {
|
|
862
|
-
try {
|
|
863
|
-
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
864
|
-
await yielded();
|
|
865
|
-
await unit();
|
|
866
|
-
queueReadiness();
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
finally {
|
|
870
|
-
indexing = false;
|
|
871
|
-
}
|
|
872
|
-
})().catch(() => { });
|
|
999
|
+
});
|
|
1000
|
+
if ('pending' in answered)
|
|
1001
|
+
return answered.pending;
|
|
1002
|
+
const held = answered.cover;
|
|
1003
|
+
return { ...answered.rows, coverage: held.coverage };
|
|
873
1004
|
}
|
|
874
1005
|
function publishChangeSet(set) {
|
|
875
1006
|
applyChangeSet(set);
|
|
@@ -879,7 +1010,10 @@ export function composeRuntime(options) {
|
|
|
879
1010
|
for (const locator of set.queues)
|
|
880
1011
|
emitQueue(locator);
|
|
881
1012
|
if (set.drafts.length > 0 ||
|
|
882
|
-
set.documents.some((change) =>
|
|
1013
|
+
set.documents.some((change) => {
|
|
1014
|
+
const held = roomDrafts.get(change.locator);
|
|
1015
|
+
return held !== undefined && (change.path === null || held.has(change.path));
|
|
1016
|
+
})) {
|
|
883
1017
|
publishRoomDrafts(false);
|
|
884
1018
|
}
|
|
885
1019
|
for (const app of set.presentations)
|
|
@@ -892,9 +1026,22 @@ export function composeRuntime(options) {
|
|
|
892
1026
|
if (reason === 'revoked-authorization') {
|
|
893
1027
|
await exclusive(async () => {
|
|
894
1028
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
1029
|
+
const records = [...replicas].map(([locator, held]) => ({
|
|
1030
|
+
locator,
|
|
1031
|
+
held,
|
|
1032
|
+
record: { ...held.record, model: revoked, selectionModel: revoked },
|
|
1033
|
+
}));
|
|
1034
|
+
let answer = null;
|
|
895
1035
|
try {
|
|
896
1036
|
const local = await openLocal();
|
|
897
|
-
await local.
|
|
1037
|
+
answer = await local.commit({
|
|
1038
|
+
rooms: records.map(({ locator, record }) => ({
|
|
1039
|
+
locator,
|
|
1040
|
+
key: record.key,
|
|
1041
|
+
record: storedRecord(record),
|
|
1042
|
+
composition: compositionOf(record),
|
|
1043
|
+
})),
|
|
1044
|
+
});
|
|
898
1045
|
}
|
|
899
1046
|
catch {
|
|
900
1047
|
journal.record('model revocation persistence failed');
|
|
@@ -910,16 +1057,16 @@ export function composeRuntime(options) {
|
|
|
910
1057
|
liveModels.delete(key);
|
|
911
1058
|
}
|
|
912
1059
|
const changed = [];
|
|
913
|
-
for (const
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1060
|
+
for (const { locator, held, record } of records) {
|
|
1061
|
+
if (replicas.get(locator) !== held)
|
|
1062
|
+
continue;
|
|
1063
|
+
const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
|
|
1064
|
+
replicas.set(locator, { ...held, record, indexed });
|
|
1065
|
+
}
|
|
1066
|
+
for (const locator of [...rooms.keys()]) {
|
|
1067
|
+
const room = rooms.get(locator);
|
|
1068
|
+
if (room === undefined)
|
|
1069
|
+
continue;
|
|
923
1070
|
rooms.set(locator, {
|
|
924
1071
|
...room,
|
|
925
1072
|
model: modelTypes(revoked),
|
|
@@ -927,9 +1074,10 @@ export function composeRuntime(options) {
|
|
|
927
1074
|
? null
|
|
928
1075
|
: { ...room.typedSelection, model: modelTypes(revoked) },
|
|
929
1076
|
});
|
|
930
|
-
changed.push(changeSet(locator,
|
|
1077
|
+
changed.push(changeSet(locator, { documents: null, room: true }));
|
|
931
1078
|
}
|
|
932
1079
|
publishChangeSet(mergeChangeSets(changed));
|
|
1080
|
+
prepare();
|
|
933
1081
|
});
|
|
934
1082
|
}
|
|
935
1083
|
}
|
|
@@ -960,41 +1108,57 @@ export function composeRuntime(options) {
|
|
|
960
1108
|
model: modelTypes(modelOf(room.locator, 'typed-selection')),
|
|
961
1109
|
},
|
|
962
1110
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
963
|
-
roomVersion: replicas.get(room.locator)?.
|
|
1111
|
+
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
964
1112
|
synchronizationAvailable,
|
|
965
1113
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
966
1114
|
});
|
|
967
1115
|
}
|
|
968
|
-
async function applyDiscovery(found, started) {
|
|
1116
|
+
async function applyDiscovery(found, started, covered) {
|
|
1117
|
+
const lockedBefore = lockedRooms();
|
|
969
1118
|
for (const room of found) {
|
|
970
1119
|
const fence = fences.get(room.locator);
|
|
971
1120
|
if (fence !== undefined && fence.erased && fence.at < started)
|
|
972
1121
|
fences.delete(room.locator);
|
|
973
1122
|
}
|
|
1123
|
+
await loseRooms(found
|
|
1124
|
+
.filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
|
|
1125
|
+
.map((room) => room.locator));
|
|
974
1126
|
const listed = found.filter((room) => !fences.has(room.locator));
|
|
975
1127
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
976
1128
|
for (const locator of listedLocators)
|
|
977
1129
|
scopedRefusals.delete(locator);
|
|
978
|
-
|
|
1130
|
+
if (covered === undefined)
|
|
1131
|
+
discovered.clear();
|
|
1132
|
+
else
|
|
1133
|
+
for (const locator of covered)
|
|
1134
|
+
discovered.delete(locator);
|
|
979
1135
|
for (const room of listed)
|
|
980
1136
|
discovered.set(room.locator, room);
|
|
981
|
-
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
|
|
1137
|
+
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
|
|
982
1138
|
if (options.adapter.exhaustiveDiscovery === true)
|
|
983
1139
|
await loseRooms(unlisted);
|
|
984
1140
|
else
|
|
985
1141
|
unlistedKept(unlisted);
|
|
986
1142
|
for (const room of listed) {
|
|
987
|
-
const held = replicas.get(room.locator)?.
|
|
1143
|
+
const held = replicas.get(room.locator)?.record;
|
|
988
1144
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
989
1145
|
list(held, false);
|
|
990
1146
|
continue;
|
|
991
1147
|
}
|
|
992
1148
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
993
1149
|
}
|
|
1150
|
+
confirmLocks(listed);
|
|
994
1151
|
roomsListed = true;
|
|
995
1152
|
emitRooms();
|
|
1153
|
+
relocked(lockedBefore);
|
|
996
1154
|
return listed;
|
|
997
1155
|
}
|
|
1156
|
+
function heldLocally(locator) {
|
|
1157
|
+
return (replicas.has(locator) ||
|
|
1158
|
+
roomDrafts.has(locator) ||
|
|
1159
|
+
transfers.has(locator) ||
|
|
1160
|
+
queuesWithoutReplica.has(locator));
|
|
1161
|
+
}
|
|
998
1162
|
function unlistedHeld(listed) {
|
|
999
1163
|
return [
|
|
1000
1164
|
...new Set([
|
|
@@ -1007,7 +1171,7 @@ export function composeRuntime(options) {
|
|
|
1007
1171
|
}
|
|
1008
1172
|
function unlistedKept(unlisted) {
|
|
1009
1173
|
for (const locator of unlisted) {
|
|
1010
|
-
const held = replicas.get(locator)?.
|
|
1174
|
+
const held = replicas.get(locator)?.record;
|
|
1011
1175
|
if (held !== undefined)
|
|
1012
1176
|
list(held, false);
|
|
1013
1177
|
else
|
|
@@ -1016,16 +1180,16 @@ export function composeRuntime(options) {
|
|
|
1016
1180
|
}
|
|
1017
1181
|
function fence(locators) {
|
|
1018
1182
|
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1019
|
-
const removed = fresh.map((locator) =>
|
|
1020
|
-
|
|
1021
|
-
queue: true,
|
|
1183
|
+
const removed = fresh.map((locator) => ({
|
|
1184
|
+
locator,
|
|
1022
1185
|
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
1023
1186
|
}));
|
|
1024
1187
|
for (const locator of fresh) {
|
|
1025
1188
|
fences.set(locator, { at: discoveries, erased: false });
|
|
1026
1189
|
lastFenced.set(locator, ++fencings);
|
|
1190
|
+
confirmed.delete(locator);
|
|
1027
1191
|
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1028
|
-
pendings.delete(batch.id);
|
|
1192
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1029
1193
|
replicas.delete(locator);
|
|
1030
1194
|
queuesWithoutReplica.delete(locator);
|
|
1031
1195
|
rooms.delete(locator);
|
|
@@ -1037,8 +1201,7 @@ export function composeRuntime(options) {
|
|
|
1037
1201
|
unreadable.delete(locator);
|
|
1038
1202
|
stale.delete(locator);
|
|
1039
1203
|
repairable.delete(locator);
|
|
1040
|
-
|
|
1041
|
-
paused.delete(locator);
|
|
1204
|
+
unpreparable.delete(locator);
|
|
1042
1205
|
scopedRefusals.delete(locator);
|
|
1043
1206
|
sourceRefusals.delete(locator);
|
|
1044
1207
|
declarationRefusals.delete(locator);
|
|
@@ -1052,26 +1215,48 @@ export function composeRuntime(options) {
|
|
|
1052
1215
|
}
|
|
1053
1216
|
if (fresh.length === 0)
|
|
1054
1217
|
return fresh;
|
|
1055
|
-
publishChangeSet(mergeChangeSets(removed));
|
|
1056
|
-
|
|
1057
|
-
prepared.delete(locator);
|
|
1058
|
-
searchIndex.forget(fresh);
|
|
1059
|
-
readSide.recover();
|
|
1218
|
+
publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
|
|
1219
|
+
liveQueries.recover();
|
|
1060
1220
|
moveTo({ ...status, ...attention() });
|
|
1061
1221
|
return fresh;
|
|
1062
1222
|
}
|
|
1063
|
-
async function
|
|
1064
|
-
await local.erase(locators);
|
|
1223
|
+
async function eraseFenced(locators) {
|
|
1065
1224
|
for (const locator of locators) {
|
|
1225
|
+
while (await durably((local) => local.erase(locator, ERASE_BATCH)))
|
|
1226
|
+
await yielded();
|
|
1066
1227
|
const held = fences.get(locator);
|
|
1067
1228
|
if (held !== undefined)
|
|
1068
1229
|
held.erased = true;
|
|
1069
1230
|
}
|
|
1070
1231
|
}
|
|
1232
|
+
let sweeping = false;
|
|
1233
|
+
function sweep() {
|
|
1234
|
+
if (sweeping || closed)
|
|
1235
|
+
return;
|
|
1236
|
+
sweeping = true;
|
|
1237
|
+
void (async () => {
|
|
1238
|
+
try {
|
|
1239
|
+
while (!closed && (await durably((local) => local.erase(null, ERASE_BATCH)))) {
|
|
1240
|
+
await yielded();
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
catch {
|
|
1244
|
+
journal.record('erasure failed');
|
|
1245
|
+
}
|
|
1246
|
+
finally {
|
|
1247
|
+
sweeping = false;
|
|
1248
|
+
}
|
|
1249
|
+
})();
|
|
1250
|
+
}
|
|
1071
1251
|
async function loseRooms(locators) {
|
|
1072
1252
|
const fresh = fence(locators);
|
|
1073
|
-
if (fresh.length
|
|
1074
|
-
|
|
1253
|
+
if (fresh.length === 0)
|
|
1254
|
+
return;
|
|
1255
|
+
await durably(async (local) => {
|
|
1256
|
+
await local.fence(fresh);
|
|
1257
|
+
await unpair(local);
|
|
1258
|
+
});
|
|
1259
|
+
await eraseFenced(fresh);
|
|
1075
1260
|
}
|
|
1076
1261
|
function restored(key, persisted) {
|
|
1077
1262
|
const prior = models.get(key);
|
|
@@ -1092,54 +1277,56 @@ export function composeRuntime(options) {
|
|
|
1092
1277
|
return model;
|
|
1093
1278
|
}
|
|
1094
1279
|
async function hydrate(opened) {
|
|
1095
|
-
const [
|
|
1096
|
-
opened.
|
|
1280
|
+
const [held, queues, kept, heldConversations, chatDrafts] = await Promise.all([
|
|
1281
|
+
opened.opened(),
|
|
1097
1282
|
opened.queues(),
|
|
1098
1283
|
opened.roomDrafts(),
|
|
1099
|
-
opened.presentation(),
|
|
1100
1284
|
opened.conversations(),
|
|
1101
1285
|
opened.chatDrafts(),
|
|
1102
|
-
opened.lastCompleted(),
|
|
1103
|
-
opened.providerLimits(),
|
|
1104
|
-
opened.capabilities(),
|
|
1105
|
-
opened.member(),
|
|
1106
|
-
opened.transfers(),
|
|
1107
|
-
opened.targets(),
|
|
1108
1286
|
]);
|
|
1109
|
-
|
|
1287
|
+
const records = [...held.rooms.values()];
|
|
1288
|
+
const counts = await opened.counts(records.map(({ key, typedSelection }) => ({ key, roots: typedSelection?.roots ?? [] })));
|
|
1289
|
+
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1290
|
+
targets = held.targets;
|
|
1110
1291
|
transfers.clear();
|
|
1111
|
-
for (const record of
|
|
1112
|
-
transfers.set(
|
|
1113
|
-
limits =
|
|
1114
|
-
capabilities =
|
|
1115
|
-
member =
|
|
1292
|
+
for (const [locator, record] of held.transfers)
|
|
1293
|
+
transfers.set(locator, record);
|
|
1294
|
+
limits = held.providerLimits;
|
|
1295
|
+
capabilities = held.capabilities;
|
|
1296
|
+
member = held.member;
|
|
1116
1297
|
roomDrafts.clear();
|
|
1117
1298
|
for (const record of kept)
|
|
1118
1299
|
rememberRoomDraft(record);
|
|
1119
1300
|
presentations.clear();
|
|
1120
|
-
for (const record of
|
|
1301
|
+
for (const record of held.presentation)
|
|
1121
1302
|
presentations.set(record.app, record.value);
|
|
1122
|
-
conversations.hydrate(
|
|
1123
|
-
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1303
|
+
conversations.hydrate(heldConversations, chatDrafts);
|
|
1124
1304
|
pendings.clear();
|
|
1125
1305
|
for (const queue of queues) {
|
|
1126
|
-
for (const { batch, state } of queue.pending)
|
|
1127
|
-
pendings.set(batch.id, state);
|
|
1306
|
+
for (const { batch, state } of queue.pending) {
|
|
1307
|
+
pendings.set(pendingKey(queue.locator, batch.id), state);
|
|
1308
|
+
}
|
|
1128
1309
|
}
|
|
1129
|
-
const previousReplicas = new Map(replicas);
|
|
1130
1310
|
replicas.clear();
|
|
1131
1311
|
queuesWithoutReplica.clear();
|
|
1132
|
-
const replicated = new Set(records.map((record) => record.locator));
|
|
1133
1312
|
for (const [locator, queue] of queuedBatches) {
|
|
1134
|
-
if (!
|
|
1313
|
+
if (!held.rooms.has(locator))
|
|
1135
1314
|
queuesWithoutReplica.set(locator, queue);
|
|
1136
1315
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
const
|
|
1141
|
-
const
|
|
1142
|
-
|
|
1316
|
+
records.forEach((stored, index) => {
|
|
1317
|
+
if (fences.has(stored.locator))
|
|
1318
|
+
return;
|
|
1319
|
+
const model = restored(modelKey(stored.locator, 'collection'), stored.model);
|
|
1320
|
+
const selectionModel = restored(modelKey(stored.locator, 'typed-selection'), stored.selectionModel);
|
|
1321
|
+
const record = { ...stored, model, selectionModel };
|
|
1322
|
+
replicas.set(record.locator, {
|
|
1323
|
+
record,
|
|
1324
|
+
queue: queuedBatches.get(record.locator) ?? [],
|
|
1325
|
+
overlaid: held.overlaid.get(record.key) ?? new Set(),
|
|
1326
|
+
count: counts[index] ?? 0,
|
|
1327
|
+
confirmed: 0,
|
|
1328
|
+
indexed: !held.unindexed.has(record.key),
|
|
1329
|
+
});
|
|
1143
1330
|
const existing = rooms.get(record.locator);
|
|
1144
1331
|
if (existing === undefined)
|
|
1145
1332
|
list(record, false);
|
|
@@ -1158,11 +1345,15 @@ export function composeRuntime(options) {
|
|
|
1158
1345
|
},
|
|
1159
1346
|
});
|
|
1160
1347
|
}
|
|
1161
|
-
}
|
|
1348
|
+
});
|
|
1349
|
+
for (const key of held.orphans)
|
|
1350
|
+
await opened.release(key);
|
|
1351
|
+
if (held.erasing || held.orphans.length > 0)
|
|
1352
|
+
queueMicrotask(sweep);
|
|
1162
1353
|
return {
|
|
1163
|
-
lastCompleted,
|
|
1354
|
+
lastCompleted: held.lastCompleted,
|
|
1164
1355
|
locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
|
|
1165
|
-
read: mergeChangeSets([...
|
|
1356
|
+
read: mergeChangeSets([...replicas.keys()].map((locator) => changeSet(locator, { documents: null }))),
|
|
1166
1357
|
};
|
|
1167
1358
|
}
|
|
1168
1359
|
async function readState(opened) {
|
|
@@ -1181,8 +1372,7 @@ export function composeRuntime(options) {
|
|
|
1181
1372
|
async function open() {
|
|
1182
1373
|
if (closed)
|
|
1183
1374
|
throw new Error('runtime closed');
|
|
1184
|
-
const opened =
|
|
1185
|
-
store = opened;
|
|
1375
|
+
const opened = openRuntimeStore(isFenced, database);
|
|
1186
1376
|
const interrupted = await opened.fences();
|
|
1187
1377
|
for (const locator of interrupted) {
|
|
1188
1378
|
fences.set(locator, { at: discoveries, erased: false });
|
|
@@ -1201,9 +1391,17 @@ export function composeRuntime(options) {
|
|
|
1201
1391
|
emitStorageLoss();
|
|
1202
1392
|
}
|
|
1203
1393
|
const lastCompleted = await readState(opened);
|
|
1204
|
-
if (interrupted.length > 0)
|
|
1205
|
-
|
|
1394
|
+
if (interrupted.length > 0) {
|
|
1395
|
+
queueMicrotask(() => {
|
|
1396
|
+
void eraseFenced(interrupted).catch(() => {
|
|
1397
|
+
journal.record('erasure failed');
|
|
1398
|
+
});
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1206
1401
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
1402
|
+
void exclusive(() => unpair(opened)).catch(() => {
|
|
1403
|
+
journal.record('unpair failed');
|
|
1404
|
+
});
|
|
1207
1405
|
return opened;
|
|
1208
1406
|
}
|
|
1209
1407
|
function durably(work) {
|
|
@@ -1220,82 +1418,283 @@ export function composeRuntime(options) {
|
|
|
1220
1418
|
return opening;
|
|
1221
1419
|
}
|
|
1222
1420
|
function exclusive(work) {
|
|
1421
|
+
gathering = null;
|
|
1223
1422
|
const next = mutations.then(work, work);
|
|
1224
1423
|
mutations = next.then(() => undefined, () => undefined);
|
|
1225
1424
|
return next;
|
|
1226
1425
|
}
|
|
1227
|
-
function
|
|
1228
|
-
const
|
|
1229
|
-
|
|
1426
|
+
function gathered(key, input, merge, work) {
|
|
1427
|
+
const tail = gathering;
|
|
1428
|
+
if (tail !== null && tail.key === key) {
|
|
1429
|
+
const merged = merge(tail.input, input);
|
|
1430
|
+
if (merged !== null) {
|
|
1431
|
+
tail.input = merged;
|
|
1432
|
+
return tail.done;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
const entry = { key, input, done: Promise.resolve() };
|
|
1436
|
+
entry.done = exclusive(() => {
|
|
1437
|
+
if (gathering === entry)
|
|
1438
|
+
gathering = null;
|
|
1439
|
+
return work(entry.input);
|
|
1440
|
+
});
|
|
1441
|
+
gathering = entry;
|
|
1442
|
+
return entry.done;
|
|
1443
|
+
}
|
|
1444
|
+
function withdrawable(locator) {
|
|
1445
|
+
return (batch) => {
|
|
1446
|
+
const state = pendings.get(pendingKey(locator, batch.id));
|
|
1447
|
+
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
const REWRITE_LINKS = ['field', 'body'];
|
|
1451
|
+
const CHECKED_LINKS = ['field', 'section'];
|
|
1452
|
+
async function sliceAt(record, view, queue, extra = [], moved = []) {
|
|
1453
|
+
const local = await openLocal();
|
|
1454
|
+
const needed = new Set([...touchedBy(queue), ...extra]);
|
|
1455
|
+
const movedPaths = [...new Set([...movedBy(queue, record.locator), ...moved])];
|
|
1456
|
+
if (movedPaths.length > 0) {
|
|
1457
|
+
for (const { path } of await local.referrers(record.key, view, movedPaths, REWRITE_LINKS)) {
|
|
1458
|
+
needed.add(path);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
const paths = [...needed];
|
|
1462
|
+
return stepped(slicing(await local.entries(record.key, view, paths), paths));
|
|
1463
|
+
}
|
|
1464
|
+
async function landedWhole(locator, batch, confirmedLayer, current) {
|
|
1465
|
+
if (!landed(batch, confirmedLayer, current))
|
|
1466
|
+
return false;
|
|
1467
|
+
const partner = partnerOf(batch, locator);
|
|
1468
|
+
if (partner === null)
|
|
1469
|
+
return true;
|
|
1470
|
+
const other = replicas.get(partner);
|
|
1471
|
+
const half = other?.queue.find((one) => one.id === batch.id);
|
|
1472
|
+
if (other === undefined || half === undefined)
|
|
1473
|
+
return false;
|
|
1474
|
+
return landed(half, await sliceAt(other.record, 'confirmed', [half]));
|
|
1475
|
+
}
|
|
1476
|
+
function carriedBack(locator, unlanded, written) {
|
|
1477
|
+
const back = new Map();
|
|
1478
|
+
const taken = [];
|
|
1479
|
+
for (const batch of unlanded) {
|
|
1480
|
+
const between = batch.between;
|
|
1481
|
+
if (between === undefined)
|
|
1482
|
+
continue;
|
|
1483
|
+
const source = between.source.room;
|
|
1484
|
+
const destination = between.destination.room;
|
|
1485
|
+
for (const { path: to } of pendings.get(pendingKey(destination, batch.id))?.drafts ?? []) {
|
|
1486
|
+
const record = roomDrafts.get(destination)?.get(to);
|
|
1487
|
+
const path = between.moves.find((move) => move.to === to)?.path;
|
|
1488
|
+
if (record === undefined || path === undefined)
|
|
1489
|
+
continue;
|
|
1490
|
+
const [into, key] = source === locator ? [written, path] : [back, documentKey(source, path)];
|
|
1491
|
+
const existing = into.get(key) ?? roomDrafts.get(source)?.get(path);
|
|
1492
|
+
taken.push({ locator: destination, path: to });
|
|
1493
|
+
into.set(key, {
|
|
1494
|
+
locator: source,
|
|
1495
|
+
path,
|
|
1496
|
+
revision: roomDraftRevision(),
|
|
1497
|
+
parts: returnedInto(existing?.parts, record.parts),
|
|
1498
|
+
returned: existing?.returned ?? null,
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
const records = [...back.values()];
|
|
1503
|
+
const elsewhere = taken.filter((one) => one.locator !== locator);
|
|
1504
|
+
const rooms = new Set([...records, ...elsewhere].map((one) => one.locator));
|
|
1505
|
+
return {
|
|
1506
|
+
taken: taken.filter((one) => one.locator === locator).map(({ path }) => path),
|
|
1507
|
+
writes: [...rooms].map((room) => ({
|
|
1508
|
+
locator: room,
|
|
1509
|
+
roomDrafts: records.filter((record) => record.locator === room),
|
|
1510
|
+
consumedRoomDrafts: elsewhere.filter((one) => one.locator === room).map(({ path }) => path),
|
|
1511
|
+
})),
|
|
1512
|
+
changes: [...rooms].map((room) => changeSet(room, {
|
|
1513
|
+
drafts: [...records, ...elsewhere]
|
|
1514
|
+
.filter((one) => one.locator === room)
|
|
1515
|
+
.map(({ path }) => path),
|
|
1516
|
+
})),
|
|
1517
|
+
remember() {
|
|
1518
|
+
for (const one of taken)
|
|
1519
|
+
forgetRoomDraft(one.locator, one.path);
|
|
1520
|
+
for (const record of records)
|
|
1521
|
+
rememberRoomDraft(record);
|
|
1522
|
+
},
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
function committedRoom(locator, answer) {
|
|
1526
|
+
const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
|
|
1527
|
+
if (!indexed)
|
|
1528
|
+
prepare([locator]);
|
|
1529
|
+
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
|
|
1530
|
+
}
|
|
1531
|
+
function replayed(record, confirmedLayer, queue) {
|
|
1532
|
+
return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
|
|
1533
|
+
}
|
|
1534
|
+
async function overlayDiff(record, before, visible, touched) {
|
|
1535
|
+
const kept = before !== undefined && before.record.key === record.key
|
|
1536
|
+
? before.overlaid
|
|
1537
|
+
: new Set();
|
|
1538
|
+
return {
|
|
1539
|
+
overlay: {
|
|
1540
|
+
set: await stepped(overlayOf(visible, touched)),
|
|
1541
|
+
cleared: [...kept].filter((path) => !touched.has(path)),
|
|
1542
|
+
},
|
|
1543
|
+
touched,
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
async function settle(local, locator, before, record, queue, settling = {}) {
|
|
1547
|
+
return together(local, [await settlement(local, locator, before, record, queue, settling)]);
|
|
1548
|
+
}
|
|
1549
|
+
function unpairedIn(locator, batch) {
|
|
1550
|
+
if (!queueIn(locator).some((held) => held.id === batch.id))
|
|
1551
|
+
return false;
|
|
1552
|
+
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
1553
|
+
return false;
|
|
1554
|
+
return !leavingIn(batch, locator) || withdrawable(locator)(batch);
|
|
1555
|
+
}
|
|
1556
|
+
async function together(local, settlements) {
|
|
1557
|
+
const settled = new Set(settlements.map(({ locator }) => locator));
|
|
1558
|
+
const all = [...settlements];
|
|
1559
|
+
for (let wave = settlements; wave.length > 0;) {
|
|
1560
|
+
const partnered = new Map();
|
|
1561
|
+
for (const { locator, left } of wave) {
|
|
1562
|
+
for (const batch of left) {
|
|
1563
|
+
const partner = partnerOf(batch, locator);
|
|
1564
|
+
if (partner === null || settled.has(partner) || !unpairedIn(partner, batch))
|
|
1565
|
+
continue;
|
|
1566
|
+
partnered.set(partner, [...(partnered.get(partner) ?? []), batch]);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
const partners = [];
|
|
1570
|
+
for (const [partner, batches] of partnered) {
|
|
1571
|
+
settled.add(partner);
|
|
1572
|
+
partners.push(await unpairing(local, partner, batches));
|
|
1573
|
+
}
|
|
1574
|
+
all.push(...partners);
|
|
1575
|
+
wave = partners;
|
|
1576
|
+
}
|
|
1577
|
+
const answer = await local.commit({
|
|
1578
|
+
rooms: all.flatMap(({ commit }) => commit.rooms),
|
|
1579
|
+
writes: all.flatMap(({ commit }) => commit.writes),
|
|
1580
|
+
});
|
|
1581
|
+
return mergeChangeSets(all.map((one) => one.apply(answer)));
|
|
1230
1582
|
}
|
|
1231
|
-
async function
|
|
1583
|
+
async function settlement(local, locator, before, record, queue, settling = {}) {
|
|
1584
|
+
const view = settling.view ?? 'confirmed';
|
|
1232
1585
|
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
1233
|
-
const
|
|
1234
|
-
const
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1586
|
+
const overrides = settling.changes;
|
|
1587
|
+
const read = await sliceAt(record, view, queue, [
|
|
1588
|
+
...(before !== undefined && before.record.key === record.key ? before.overlaid : []),
|
|
1589
|
+
...(overrides?.set.map(({ path }) => path) ?? []),
|
|
1590
|
+
...(overrides?.removed ?? []),
|
|
1591
|
+
]);
|
|
1592
|
+
const confirmedLayer = overrides === undefined
|
|
1593
|
+
? read
|
|
1594
|
+
: await stepped(slicingWith(read, overrides.set, overrides.removed));
|
|
1595
|
+
const composition = compositionOf(record);
|
|
1596
|
+
const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
|
|
1597
|
+
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1598
|
+
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
1599
|
+
const unqueued = [];
|
|
1600
|
+
for (const batch of withdrawn) {
|
|
1601
|
+
unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
|
|
1602
|
+
}
|
|
1239
1603
|
const returning = [
|
|
1240
|
-
...[...unqueued].reverse(),
|
|
1241
|
-
|
|
1604
|
+
...[...unqueued].reverse().map(({ batch, landed: held }) => ({
|
|
1605
|
+
batch,
|
|
1606
|
+
returned: localSaves || held ? null : returnedBy('withdrawn'),
|
|
1607
|
+
})),
|
|
1608
|
+
...(settling.leaving ?? []),
|
|
1242
1609
|
];
|
|
1243
1610
|
const written = new Map();
|
|
1244
|
-
for (const { batch, returned } of returning) {
|
|
1245
|
-
if (
|
|
1611
|
+
for (const { batch, returned: reason } of returning) {
|
|
1612
|
+
if (reason === null || batch.between?.destination.room === locator)
|
|
1246
1613
|
continue;
|
|
1247
|
-
for (const saved of pendings.get(batch.id)?.drafts ?? []) {
|
|
1614
|
+
for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
|
|
1248
1615
|
const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
|
|
1249
1616
|
written.set(saved.path, {
|
|
1250
1617
|
locator,
|
|
1251
1618
|
path: saved.path,
|
|
1252
1619
|
revision: roomDraftRevision(),
|
|
1253
1620
|
parts: returnedInto(existing?.parts, saved.parts),
|
|
1254
|
-
returned,
|
|
1621
|
+
returned: reason,
|
|
1255
1622
|
});
|
|
1256
1623
|
}
|
|
1257
1624
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1625
|
+
const carried = carriedBack(locator, [
|
|
1626
|
+
...[...unqueued].reverse().flatMap(({ batch, landed: held }) => (held ? [] : [batch])),
|
|
1627
|
+
...(settling.leaving ?? []).flatMap(({ batch, returned }) => returned === null ? [] : [batch]),
|
|
1628
|
+
], written);
|
|
1629
|
+
const replayedLayer = await replayed(record, confirmedLayer, kept);
|
|
1630
|
+
const { overlay, touched } = await overlayDiff(record, before, replayedLayer.visible, replayedLayer.touched);
|
|
1631
|
+
const commit = {
|
|
1632
|
+
rooms: [
|
|
1633
|
+
{
|
|
1634
|
+
locator,
|
|
1635
|
+
key: record.key,
|
|
1636
|
+
record: storedRecord(record),
|
|
1637
|
+
roots: record.typedSelection?.roots ?? [],
|
|
1638
|
+
...settling.commit,
|
|
1639
|
+
...(overrides === undefined
|
|
1640
|
+
? {}
|
|
1641
|
+
: { confirmed: { set: overrides.set, removed: overrides.removed } }),
|
|
1642
|
+
overlay,
|
|
1643
|
+
},
|
|
1644
|
+
],
|
|
1645
|
+
writes: [
|
|
1646
|
+
{
|
|
1647
|
+
locator,
|
|
1648
|
+
queue: kept,
|
|
1649
|
+
founded,
|
|
1650
|
+
roomDrafts: [...written.values()],
|
|
1651
|
+
consumedRoomDrafts: carried.taken,
|
|
1652
|
+
},
|
|
1653
|
+
...carried.writes,
|
|
1654
|
+
],
|
|
1655
|
+
};
|
|
1656
|
+
const staying = new Set(kept.map((batch) => batch.id));
|
|
1657
|
+
return {
|
|
1658
|
+
locator,
|
|
1659
|
+
left: previousQueue.filter((batch) => !staying.has(batch.id)),
|
|
1660
|
+
commit,
|
|
1661
|
+
apply: (answer) => {
|
|
1662
|
+
for (const batch of previousQueue) {
|
|
1663
|
+
if (!staying.has(batch.id))
|
|
1664
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1665
|
+
}
|
|
1666
|
+
for (const draft of written.values())
|
|
1667
|
+
rememberRoomDraft(draft);
|
|
1668
|
+
carried.remember();
|
|
1669
|
+
const drafts = [...written.keys(), ...carried.taken];
|
|
1279
1670
|
queuesWithoutReplica.delete(locator);
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1671
|
+
for (const { batch, landed: held } of unqueued) {
|
|
1672
|
+
const outcome = localSaves ? 'dropped' : held ? 'landed' : 'withdrawn';
|
|
1673
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1674
|
+
}
|
|
1675
|
+
const changed = answer.changed[locator];
|
|
1676
|
+
const confirmedChanged = settling.commit?.published !== undefined ||
|
|
1677
|
+
settling.commit?.refreshed === true ||
|
|
1678
|
+
overrides !== undefined;
|
|
1679
|
+
const { count, indexed } = committedRoom(locator, answer);
|
|
1680
|
+
replicas.set(locator, {
|
|
1681
|
+
record,
|
|
1682
|
+
queue: kept,
|
|
1683
|
+
overlaid: touched,
|
|
1684
|
+
count,
|
|
1685
|
+
confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
|
|
1686
|
+
indexed,
|
|
1687
|
+
});
|
|
1688
|
+
return mergeChangeSets([
|
|
1689
|
+
changeSet(locator, {
|
|
1690
|
+
documents: changed === undefined ? [] : changed,
|
|
1691
|
+
room: settling.room,
|
|
1692
|
+
queue: queueChanged,
|
|
1693
|
+
drafts,
|
|
1694
|
+
}),
|
|
1695
|
+
...carried.changes,
|
|
1696
|
+
]);
|
|
1697
|
+
},
|
|
1299
1698
|
};
|
|
1300
1699
|
}
|
|
1301
1700
|
function adoptModels(locator, record) {
|
|
@@ -1316,7 +1715,7 @@ export function composeRuntime(options) {
|
|
|
1316
1715
|
const taken = (answer, previous, attached) => {
|
|
1317
1716
|
if (answer === undefined)
|
|
1318
1717
|
return previous;
|
|
1319
|
-
const { state, refused } = attachedModel(answer, previous);
|
|
1718
|
+
const { state, refused } = attachedModel(answer, previous, attached === 'collection' ? roomTypes : NO_ROOM_TYPES);
|
|
1320
1719
|
if (refused !== null) {
|
|
1321
1720
|
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
1322
1721
|
journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
|
|
@@ -1329,70 +1728,85 @@ export function composeRuntime(options) {
|
|
|
1329
1728
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
1330
1729
|
};
|
|
1331
1730
|
}
|
|
1332
|
-
function publish(local, room,
|
|
1333
|
-
const
|
|
1334
|
-
return exclusive(async () => {
|
|
1335
|
-
if (stored !== undefined) {
|
|
1336
|
-
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1337
|
-
}
|
|
1731
|
+
function publish(local, room, record, landing, live) {
|
|
1732
|
+
const landed = exclusive(async () => {
|
|
1338
1733
|
if (!live()) {
|
|
1339
1734
|
caughtUpTo(room.locator);
|
|
1340
1735
|
return true;
|
|
1341
1736
|
}
|
|
1342
1737
|
const prior = replicas.get(room.locator);
|
|
1343
|
-
if (
|
|
1344
|
-
(prior
|
|
1345
|
-
|
|
1738
|
+
if (landing.kind === 'refreshed' &&
|
|
1739
|
+
(prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
|
|
1740
|
+
await local.unrefresh(record.key);
|
|
1346
1741
|
return false;
|
|
1347
1742
|
}
|
|
1348
|
-
adoptModels(room.locator,
|
|
1743
|
+
adoptModels(room.locator, record);
|
|
1349
1744
|
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
1350
|
-
const roomVersion =
|
|
1745
|
+
const roomVersion = record.roomVersion;
|
|
1351
1746
|
const accepted = queue.filter((batch) => {
|
|
1352
|
-
const
|
|
1353
|
-
return
|
|
1747
|
+
const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
|
|
1748
|
+
return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
|
|
1354
1749
|
});
|
|
1355
|
-
let
|
|
1750
|
+
let set;
|
|
1356
1751
|
try {
|
|
1357
|
-
|
|
1752
|
+
set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
|
|
1753
|
+
? {
|
|
1754
|
+
room: true,
|
|
1755
|
+
view: 'refreshed',
|
|
1756
|
+
commit: { refreshed: true, composition: compositionOf(record) },
|
|
1757
|
+
}
|
|
1758
|
+
: {
|
|
1759
|
+
room: true,
|
|
1760
|
+
view: landing.refreshed ? 'refreshed' : 'confirmed',
|
|
1761
|
+
commit: {
|
|
1762
|
+
published: { from: prior?.record.key ?? null },
|
|
1763
|
+
composition: compositionOf(record),
|
|
1764
|
+
...(landing.refreshed ? { refreshed: true } : {}),
|
|
1765
|
+
},
|
|
1766
|
+
});
|
|
1358
1767
|
}
|
|
1359
1768
|
catch (error) {
|
|
1360
|
-
if (!
|
|
1769
|
+
if (!refusedForSpace(error))
|
|
1361
1770
|
throw error;
|
|
1362
|
-
await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
|
|
1363
1771
|
caughtUpTo(room.locator);
|
|
1364
1772
|
return true;
|
|
1365
1773
|
}
|
|
1366
|
-
|
|
1367
|
-
if (stored !== undefined) {
|
|
1774
|
+
if (landing.kind === 'bootstrapped') {
|
|
1368
1775
|
transfers.delete(room.locator);
|
|
1369
1776
|
catching.delete(room.locator);
|
|
1777
|
+
sweep();
|
|
1370
1778
|
}
|
|
1371
1779
|
for (const batch of accepted)
|
|
1372
|
-
pendings.delete(batch.id);
|
|
1780
|
+
pendings.delete(pendingKey(room.locator, batch.id));
|
|
1373
1781
|
stale.delete(room.locator);
|
|
1374
|
-
|
|
1782
|
+
unpreparable.delete(room.locator);
|
|
1375
1783
|
if (unreadable.delete(room.locator))
|
|
1376
1784
|
scopedRefusals.delete(room.locator);
|
|
1377
1785
|
sourceRefusals.delete(room.locator);
|
|
1378
1786
|
declarationRefusals.delete(room.locator);
|
|
1379
1787
|
list(room, true);
|
|
1380
|
-
if (prior === undefined)
|
|
1381
|
-
prepared.add(room.locator);
|
|
1382
1788
|
publishChangeSet(set);
|
|
1789
|
+
await unpair(local);
|
|
1383
1790
|
moveTo({ ...status, ...attention() });
|
|
1384
1791
|
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
1792
|
+
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1793
|
+
submitLater(room.locator);
|
|
1794
|
+
}
|
|
1385
1795
|
return true;
|
|
1386
1796
|
});
|
|
1797
|
+
return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
|
|
1387
1798
|
}
|
|
1388
|
-
async function
|
|
1799
|
+
async function savedRoom(locator) {
|
|
1389
1800
|
const local = await openLocal();
|
|
1390
|
-
await referenced(locator);
|
|
1391
1801
|
const replica = replicas.get(locator);
|
|
1392
1802
|
const room = rooms.get(locator);
|
|
1393
1803
|
if (replica === undefined || room === undefined) {
|
|
1394
1804
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1395
1805
|
}
|
|
1806
|
+
return { local, replica, room };
|
|
1807
|
+
}
|
|
1808
|
+
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
1809
|
+
const target = await savedRoom(locator);
|
|
1396
1810
|
const saved = [];
|
|
1397
1811
|
for (const { path, revision } of named) {
|
|
1398
1812
|
const record = roomDrafts.get(locator)?.get(path);
|
|
@@ -1402,12 +1816,398 @@ export function composeRuntime(options) {
|
|
|
1402
1816
|
if (!saved.some((draft) => draft.path === path))
|
|
1403
1817
|
saved.push({ path, parts: record.parts });
|
|
1404
1818
|
}
|
|
1405
|
-
|
|
1819
|
+
return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
|
|
1820
|
+
}
|
|
1821
|
+
async function saveParts(locator, named, writtenBy) {
|
|
1822
|
+
const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
|
|
1823
|
+
if (read.some(({ path }, at) => path === '' ||
|
|
1824
|
+
normalize(path) !== path ||
|
|
1825
|
+
read.findIndex((one) => one.path === path) !== at)) {
|
|
1826
|
+
throw runtimeError('room-draft-malformed');
|
|
1827
|
+
}
|
|
1828
|
+
const target = await savedRoom(locator);
|
|
1829
|
+
const saved = [];
|
|
1830
|
+
const files = [];
|
|
1831
|
+
const filed = read.filter(({ parts }) => parts.file !== undefined).map(({ path }) => path);
|
|
1832
|
+
const visible = filed.length === 0 ? undefined : await visibleOf(locator, filed);
|
|
1833
|
+
for (const { path, parts } of read) {
|
|
1834
|
+
const file = parts.file === undefined
|
|
1835
|
+
? undefined
|
|
1836
|
+
: await heldFile(parts.file.blob, binaryBase(visible, path));
|
|
1837
|
+
if (parts.file !== undefined && file !== undefined) {
|
|
1838
|
+
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1839
|
+
}
|
|
1840
|
+
saved.push({ path, parts: held(undefined, parts, file) });
|
|
1841
|
+
}
|
|
1842
|
+
return committed(locator, target, saved, new Set(), writtenBy, files);
|
|
1843
|
+
}
|
|
1844
|
+
function carriedDrafts(locator, saved, consumed) {
|
|
1845
|
+
const holding = roomDrafts.get(locator);
|
|
1846
|
+
return new Map(saved.flatMap(({ path, parts }) => {
|
|
1847
|
+
const record = holding?.get(path);
|
|
1848
|
+
if (parts.move === undefined || record === undefined || consumed.has(path))
|
|
1849
|
+
return [];
|
|
1850
|
+
const to = moveTarget(parts.move);
|
|
1851
|
+
if (to === path || holding?.has(to) === true)
|
|
1852
|
+
return [];
|
|
1853
|
+
return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
|
|
1854
|
+
}));
|
|
1855
|
+
}
|
|
1856
|
+
async function savingIn(replica, changes, extra, leaving = []) {
|
|
1857
|
+
const record = replica.record;
|
|
1858
|
+
const local = await openLocal();
|
|
1859
|
+
const paths = [
|
|
1860
|
+
...extra,
|
|
1861
|
+
...changes.flatMap((change) => change.kind === 'move' ? [change.path, change.to] : [change.path]),
|
|
1862
|
+
];
|
|
1863
|
+
const moved = [
|
|
1864
|
+
...leaving,
|
|
1865
|
+
...changes.flatMap((change) => (change.kind === 'move' ? [change.path] : [])),
|
|
1866
|
+
];
|
|
1867
|
+
const removed = changes.flatMap((change) => (change.kind === 'remove' ? [change.path] : []));
|
|
1868
|
+
const referring = removed.length === 0
|
|
1869
|
+
? []
|
|
1870
|
+
: (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
|
|
1871
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
|
|
1872
|
+
const { visible } = await replayed(record, confirmedLayer, replica.queue);
|
|
1873
|
+
const composition = compositionOf(record);
|
|
1874
|
+
return {
|
|
1875
|
+
record,
|
|
1876
|
+
confirmed: confirmedLayer,
|
|
1877
|
+
visible,
|
|
1878
|
+
*visibleOver(queue) {
|
|
1879
|
+
return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
|
|
1880
|
+
},
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
async function unmodeledIn(record) {
|
|
1884
|
+
const composition = compositionFor(record);
|
|
1885
|
+
if (available(composition))
|
|
1886
|
+
return false;
|
|
1887
|
+
const local = await openLocal();
|
|
1888
|
+
const roots = record.typedSelection?.roots ?? [];
|
|
1889
|
+
if (composition.model.status === 'unavailable' &&
|
|
1890
|
+
(await local.typedWithin(record.key, 'visible', roots, false))) {
|
|
1891
|
+
return true;
|
|
1892
|
+
}
|
|
1893
|
+
return (composition.selection !== null &&
|
|
1894
|
+
composition.selection.model.status === 'unavailable' &&
|
|
1895
|
+
(await local.typedWithin(record.key, 'visible', roots, true)));
|
|
1896
|
+
}
|
|
1897
|
+
function textDiffers(left, right) {
|
|
1898
|
+
return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
|
|
1899
|
+
!sameBinary(left?.binary ?? undefined, right?.binary ?? undefined));
|
|
1900
|
+
}
|
|
1901
|
+
function textOf(entry) {
|
|
1902
|
+
if (entry === null)
|
|
1903
|
+
return undefined;
|
|
1904
|
+
return entry.binary !== null ? null : entry.content;
|
|
1905
|
+
}
|
|
1906
|
+
function walkOf(local, key, view) {
|
|
1907
|
+
return async (after) => {
|
|
1908
|
+
const page = await local.walk(key, view, after, WALK_BYTES);
|
|
1909
|
+
return {
|
|
1910
|
+
entries: page.entries.map(({ path, content, binary }) => ({
|
|
1911
|
+
path,
|
|
1912
|
+
content: binary !== null ? null : content,
|
|
1913
|
+
})),
|
|
1914
|
+
next: page.next,
|
|
1915
|
+
};
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
function lookupOf(local, key, view) {
|
|
1919
|
+
return async (paths) => {
|
|
1920
|
+
if (paths.length === 0)
|
|
1921
|
+
return new Map();
|
|
1922
|
+
return new Map((await local.typed(key, view, paths)).map(({ path, type, binary }) => [
|
|
1923
|
+
path,
|
|
1924
|
+
{ type: type ?? undefined, binary },
|
|
1925
|
+
]));
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
function declaredOf(entry) {
|
|
1929
|
+
if (entry.binary !== null)
|
|
1930
|
+
return { type: undefined, binary: true };
|
|
1931
|
+
const read = parseFrontmatter(entry.content ?? '');
|
|
1932
|
+
return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
|
|
1933
|
+
}
|
|
1934
|
+
function overriddenWalk(walk, overrides) {
|
|
1935
|
+
const sorted = [...overrides.keys()].sort();
|
|
1936
|
+
const beyond = (path) => {
|
|
1937
|
+
if (path === null)
|
|
1938
|
+
return 0;
|
|
1939
|
+
let low = 0;
|
|
1940
|
+
let high = sorted.length;
|
|
1941
|
+
while (low < high) {
|
|
1942
|
+
const middle = (low + high) >>> 1;
|
|
1943
|
+
if (sorted[middle] <= path)
|
|
1944
|
+
low = middle + 1;
|
|
1945
|
+
else
|
|
1946
|
+
high = middle;
|
|
1947
|
+
}
|
|
1948
|
+
return low;
|
|
1949
|
+
};
|
|
1950
|
+
return async (after) => {
|
|
1951
|
+
const page = await walk(after);
|
|
1952
|
+
const end = page.next;
|
|
1953
|
+
const added = sorted
|
|
1954
|
+
.slice(beyond(after), end === null ? sorted.length : beyond(end))
|
|
1955
|
+
.flatMap((path) => {
|
|
1956
|
+
const entry = overrides.get(path);
|
|
1957
|
+
return entry === null || entry === undefined
|
|
1958
|
+
? []
|
|
1959
|
+
: [{ path, content: entry.binary !== null ? null : entry.content }];
|
|
1960
|
+
});
|
|
1961
|
+
const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
1962
|
+
return { entries, next: end };
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
function overriddenLookup(lookup, overrides) {
|
|
1966
|
+
return async (paths) => {
|
|
1967
|
+
const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
|
|
1968
|
+
for (const path of paths) {
|
|
1969
|
+
const entry = overrides.get(path);
|
|
1970
|
+
if (entry === undefined)
|
|
1971
|
+
continue;
|
|
1972
|
+
if (entry === null)
|
|
1973
|
+
known.delete(path);
|
|
1974
|
+
else
|
|
1975
|
+
known.set(path, declaredOf(entry));
|
|
1976
|
+
}
|
|
1977
|
+
return known;
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
function validatorOf(record, confirmedLayer) {
|
|
1981
|
+
return async (resulting, touched) => {
|
|
1982
|
+
const composition = compositionFor(record);
|
|
1983
|
+
const local = await openLocal();
|
|
1984
|
+
const changed = [];
|
|
1985
|
+
const overrides = new Map();
|
|
1986
|
+
const pace = paced();
|
|
1987
|
+
for (const path of new Set([...confirmedLayer.held, ...touched])) {
|
|
1988
|
+
await pace(ROW_WEIGHT);
|
|
1989
|
+
const was = entryAt(confirmedLayer, path);
|
|
1990
|
+
const now = entryAt(resulting, path);
|
|
1991
|
+
if (!textDiffers(was, now))
|
|
1992
|
+
continue;
|
|
1993
|
+
overrides.set(path, now);
|
|
1994
|
+
if (textOf(was) !== textOf(now))
|
|
1995
|
+
changed.push({ path, was: textOf(was), now: textOf(now) });
|
|
1996
|
+
}
|
|
1997
|
+
const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
|
|
1998
|
+
const clean = available(composition) &&
|
|
1999
|
+
changed.length <= CLEAN_CHANGES &&
|
|
2000
|
+
(await changedCleanly(changed, compositionOf(record), composition, {
|
|
2001
|
+
lookup,
|
|
2002
|
+
texts: textsOf(local, record.key, 'confirmed'),
|
|
2003
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2004
|
+
}));
|
|
2005
|
+
if (clean)
|
|
2006
|
+
return { verdict: 'accept' };
|
|
2007
|
+
return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
function textsOf(local, key, view) {
|
|
2011
|
+
return async (paths) => new Map((await local.entries(key, view, paths)).map(({ path, content, binary }) => [
|
|
2012
|
+
path,
|
|
2013
|
+
binary !== null ? null : content,
|
|
2014
|
+
]));
|
|
2015
|
+
}
|
|
2016
|
+
function referrersOf(local, key, view) {
|
|
2017
|
+
return async (targets) => [
|
|
2018
|
+
...new Set((await local.referrers(key, view, targets, CHECKED_LINKS)).map(({ path }) => path)),
|
|
2019
|
+
];
|
|
2020
|
+
}
|
|
2021
|
+
async function sideOf(locator, replica, room, saving) {
|
|
2022
|
+
const record = replica.record;
|
|
2023
|
+
return {
|
|
2024
|
+
room: locator,
|
|
2025
|
+
handle: room.handle,
|
|
2026
|
+
baseRoomVersion: record.roomVersion,
|
|
2027
|
+
composition: compositionFor(record),
|
|
2028
|
+
operations: room.operations,
|
|
2029
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
2030
|
+
visible: saving.visible,
|
|
2031
|
+
queued: queuedDocuments(replica.queue),
|
|
2032
|
+
unmodeled: await unmodeledIn(record),
|
|
2033
|
+
validate: validatorOf(record, saving.confirmed),
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
function movedInto(locator, saved) {
|
|
2037
|
+
const into = saved.find(({ parts }) => parts.move?.room !== undefined && parts.move.room !== locator);
|
|
2038
|
+
return into?.parts.move?.room ?? null;
|
|
2039
|
+
}
|
|
2040
|
+
async function saveCommit(local, files, rooms) {
|
|
2041
|
+
const touched = new Map();
|
|
2042
|
+
const commits = [];
|
|
2043
|
+
for (const { locator, saving, before, queue } of rooms) {
|
|
2044
|
+
const replayedLayer = await replayed(saving.record, saving.confirmed, queue);
|
|
2045
|
+
const diff = await overlayDiff(saving.record, before, replayedLayer.visible, replayedLayer.touched);
|
|
2046
|
+
touched.set(locator, diff.touched);
|
|
2047
|
+
commits.push({
|
|
2048
|
+
locator,
|
|
2049
|
+
key: saving.record.key,
|
|
2050
|
+
roots: saving.record.typedSelection?.roots ?? [],
|
|
2051
|
+
overlay: diff.overlay,
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
try {
|
|
2055
|
+
const answer = await local.commit({
|
|
2056
|
+
rooms: commits,
|
|
2057
|
+
writes: rooms.map(({ write }) => write),
|
|
2058
|
+
files,
|
|
2059
|
+
whole: true,
|
|
2060
|
+
});
|
|
2061
|
+
return { answer, touched };
|
|
2062
|
+
}
|
|
2063
|
+
catch {
|
|
2064
|
+
return null;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
function savedReplica(locator, before, queue, touched, answer) {
|
|
2068
|
+
replicas.set(locator, {
|
|
2069
|
+
...before,
|
|
2070
|
+
queue,
|
|
2071
|
+
overlaid: touched,
|
|
2072
|
+
count: answer.counts[locator] ?? before.count,
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
async function committedMove(locator, destination, { local, replica, room }, saved, consumed, writtenBy, files) {
|
|
2076
|
+
const mixed = saved.find(({ parts }) => Object.keys(parts).length !== 1 || parts.move?.room !== destination);
|
|
2077
|
+
if (mixed !== undefined) {
|
|
2078
|
+
return { outcome: 'refused', code: 'mixed-room-move', retryable: false, file: mixed.path };
|
|
2079
|
+
}
|
|
2080
|
+
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
2081
|
+
const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
|
|
2082
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, sourceSaving.visible)));
|
|
1406
2083
|
if (changed.length > 0)
|
|
1407
2084
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
1408
|
-
const
|
|
1409
|
-
const
|
|
1410
|
-
|
|
2085
|
+
const target = replicas.get(destination);
|
|
2086
|
+
const listedRoom = rooms.get(destination);
|
|
2087
|
+
if (target === undefined || listedRoom === undefined) {
|
|
2088
|
+
throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
|
|
2089
|
+
}
|
|
2090
|
+
const arriving = await savingIn(target, [], moves.map(({ to }) => to));
|
|
2091
|
+
const built = await buildMove({
|
|
2092
|
+
source: await sideOf(locator, replica, room, sourceSaving),
|
|
2093
|
+
destination: await sideOf(destination, target, listedRoom, arriving),
|
|
2094
|
+
moves,
|
|
2095
|
+
providerLimits: limits,
|
|
2096
|
+
local: localSaves,
|
|
2097
|
+
...(writtenBy === undefined ? {} : { writtenBy }),
|
|
2098
|
+
});
|
|
2099
|
+
if (built.kind === 'refused')
|
|
2100
|
+
return { outcome: 'refused', ...built.refusal };
|
|
2101
|
+
const id = built.leaving.id;
|
|
2102
|
+
const carrying = saved.flatMap(({ path, parts }) => {
|
|
2103
|
+
const record = roomDrafts.get(locator)?.get(path);
|
|
2104
|
+
const to = moveTarget(parts.move);
|
|
2105
|
+
if (record === undefined || consumed.has(path) || roomDrafts.get(destination)?.has(to)) {
|
|
2106
|
+
return [];
|
|
2107
|
+
}
|
|
2108
|
+
const moved = {
|
|
2109
|
+
...record,
|
|
2110
|
+
locator: destination,
|
|
2111
|
+
path: to,
|
|
2112
|
+
revision: roomDraftRevision(),
|
|
2113
|
+
};
|
|
2114
|
+
return [{ from: path, record: moved }];
|
|
2115
|
+
});
|
|
2116
|
+
const carried = carrying.map(({ record }) => record);
|
|
2117
|
+
const marked = carried.map(({ path }) => ({ path, parts: {} }));
|
|
2118
|
+
const left = [...consumed, ...carrying.map(({ from }) => from)];
|
|
2119
|
+
const leavingQueue = [...replica.queue, built.leaving];
|
|
2120
|
+
const arrivingQueue = [...target.queue, built.arriving];
|
|
2121
|
+
const committedBoth = await saveCommit(local, files, [
|
|
2122
|
+
{
|
|
2123
|
+
locator,
|
|
2124
|
+
saving: sourceSaving,
|
|
2125
|
+
before: replica,
|
|
2126
|
+
queue: leavingQueue,
|
|
2127
|
+
write: {
|
|
2128
|
+
locator,
|
|
2129
|
+
queue: leavingQueue,
|
|
2130
|
+
saved: new Map([[id, saved]]),
|
|
2131
|
+
consumedRoomDrafts: left,
|
|
2132
|
+
},
|
|
2133
|
+
},
|
|
2134
|
+
{
|
|
2135
|
+
locator: destination,
|
|
2136
|
+
saving: arriving,
|
|
2137
|
+
before: target,
|
|
2138
|
+
queue: arrivingQueue,
|
|
2139
|
+
write: {
|
|
2140
|
+
locator: destination,
|
|
2141
|
+
queue: arrivingQueue,
|
|
2142
|
+
saved: new Map([[id, marked]]),
|
|
2143
|
+
roomDrafts: carried,
|
|
2144
|
+
},
|
|
2145
|
+
},
|
|
2146
|
+
]);
|
|
2147
|
+
if (committedBoth === null)
|
|
2148
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2149
|
+
const { answer, touched } = committedBoth;
|
|
2150
|
+
if (answer.fenced !== null) {
|
|
2151
|
+
throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
|
|
2152
|
+
}
|
|
2153
|
+
if (fences.has(locator) || fences.has(destination)) {
|
|
2154
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
2155
|
+
}
|
|
2156
|
+
pendings.set(pendingKey(locator, id), {
|
|
2157
|
+
drafts: saved,
|
|
2158
|
+
submission: { state: 'waiting' },
|
|
2159
|
+
lost: false,
|
|
2160
|
+
});
|
|
2161
|
+
pendings.set(pendingKey(destination, id), {
|
|
2162
|
+
drafts: marked,
|
|
2163
|
+
submission: { state: 'waiting' },
|
|
2164
|
+
lost: false,
|
|
2165
|
+
});
|
|
2166
|
+
for (const path of left)
|
|
2167
|
+
forgetRoomDraft(locator, path);
|
|
2168
|
+
for (const record of carried)
|
|
2169
|
+
rememberRoomDraft(record);
|
|
2170
|
+
savedReplica(locator, replica, leavingQueue, touched.get(locator), answer);
|
|
2171
|
+
savedReplica(destination, target, arrivingQueue, touched.get(destination), answer);
|
|
2172
|
+
publishChangeSet(mergeChangeSets([
|
|
2173
|
+
changeSet(locator, {
|
|
2174
|
+
documents: answer.changed[locator] ?? [],
|
|
2175
|
+
queue: true,
|
|
2176
|
+
drafts: left,
|
|
2177
|
+
}),
|
|
2178
|
+
changeSet(destination, {
|
|
2179
|
+
documents: answer.changed[destination] ?? [],
|
|
2180
|
+
queue: true,
|
|
2181
|
+
drafts: carried.map(({ path }) => path),
|
|
2182
|
+
}),
|
|
2183
|
+
]));
|
|
2184
|
+
queuedSave(locator);
|
|
2185
|
+
return { outcome: 'saved', identifier: id };
|
|
2186
|
+
}
|
|
2187
|
+
function queuedSave(locator) {
|
|
2188
|
+
if (!localSaves)
|
|
2189
|
+
recorded = false;
|
|
2190
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
2191
|
+
submitLater(locator);
|
|
2192
|
+
}
|
|
2193
|
+
async function committed(locator, target, saved, consumed, writtenBy, files = []) {
|
|
2194
|
+
if (full)
|
|
2195
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2196
|
+
const into = movedInto(locator, saved);
|
|
2197
|
+
if (into !== null) {
|
|
2198
|
+
return committedMove(locator, into, target, saved, consumed, writtenBy, files);
|
|
2199
|
+
}
|
|
2200
|
+
const { local, replica, room } = target;
|
|
2201
|
+
const parts = saved.map(({ path, parts: held }) => changesOf(path, held));
|
|
2202
|
+
const changes = parts.flatMap(({ changes: made }) => made);
|
|
2203
|
+
const saving = await savingIn(replica, changes, [
|
|
2204
|
+
...saved.map(({ path }) => path),
|
|
2205
|
+
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
2206
|
+
]);
|
|
2207
|
+
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
|
|
2208
|
+
if (changed.length > 0)
|
|
2209
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2210
|
+
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
|
|
1411
2211
|
if (requeue.kind === 'refused')
|
|
1412
2212
|
return { outcome: 'refused', ...requeue.refusal };
|
|
1413
2213
|
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
@@ -1425,23 +2225,23 @@ export function composeRuntime(options) {
|
|
|
1425
2225
|
const withdrawing = requeue.withdrawn.size > 0;
|
|
1426
2226
|
const input = {
|
|
1427
2227
|
local: localSaves,
|
|
1428
|
-
baseRoomVersion: replica.
|
|
1429
|
-
composition: compositionFor(replica.
|
|
2228
|
+
baseRoomVersion: replica.record.roomVersion,
|
|
2229
|
+
composition: compositionFor(replica.record),
|
|
1430
2230
|
operations: room.operations,
|
|
1431
2231
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1432
2232
|
providerLimits: limits,
|
|
1433
2233
|
handle: room.handle,
|
|
1434
|
-
visible:
|
|
2234
|
+
visible: saving.visible,
|
|
1435
2235
|
changes,
|
|
1436
2236
|
bases: parts.flatMap(({ bases }) => bases),
|
|
2237
|
+
queued: queuedDocuments(replica.queue),
|
|
1437
2238
|
writtenBy,
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
binaries: replica.confirmed.record.binaries,
|
|
1441
|
-
composition: compositionOf(replica.confirmed.record),
|
|
1442
|
-
},
|
|
2239
|
+
unmodeled: await unmodeledIn(replica.record),
|
|
2240
|
+
validate: validatorOf(replica.record, saving.confirmed),
|
|
1443
2241
|
};
|
|
1444
|
-
const checked = withdrawing
|
|
2242
|
+
const checked = withdrawing
|
|
2243
|
+
? await buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
|
|
2244
|
+
: null;
|
|
1445
2245
|
if (checked?.kind === 'refused')
|
|
1446
2246
|
return { outcome: 'refused', ...checked.refusal };
|
|
1447
2247
|
const owners = [...requeue.owners].flatMap((origin) => {
|
|
@@ -1457,13 +2257,14 @@ export function composeRuntime(options) {
|
|
|
1457
2257
|
};
|
|
1458
2258
|
}
|
|
1459
2259
|
const built = !withdrawing
|
|
1460
|
-
? buildBatch(input)
|
|
2260
|
+
? await buildBatch(input)
|
|
1461
2261
|
: requeue.changes.length === 0
|
|
1462
2262
|
? null
|
|
1463
|
-
: buildBatch({
|
|
2263
|
+
: await buildBatch({
|
|
1464
2264
|
...input,
|
|
1465
|
-
visible:
|
|
2265
|
+
visible: await stepped(saving.visibleOver(requeue.queue)),
|
|
1466
2266
|
changes: requeue.changes,
|
|
2267
|
+
queued: queuedDocuments(requeue.queue),
|
|
1467
2268
|
});
|
|
1468
2269
|
if (built?.kind === 'refused')
|
|
1469
2270
|
return { outcome: 'refused', ...built.refusal };
|
|
@@ -1480,7 +2281,7 @@ export function composeRuntime(options) {
|
|
|
1480
2281
|
for (const [origin, replaced] of withdrawn) {
|
|
1481
2282
|
if (replaced === null)
|
|
1482
2283
|
continue;
|
|
1483
|
-
const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
|
|
2284
|
+
const carried = withdrawnDrafts(pendings.get(pendingKey(locator, origin))?.drafts ?? [], changes);
|
|
1484
2285
|
drafted.set(replaced.id, replaced === join?.batch
|
|
1485
2286
|
? [
|
|
1486
2287
|
...carried.map((held) => joinedDraft(held, saved)),
|
|
@@ -1494,59 +2295,77 @@ export function composeRuntime(options) {
|
|
|
1494
2295
|
for (const replaced of withdrawn.values()) {
|
|
1495
2296
|
if (replaced === null || !batches.includes(replaced))
|
|
1496
2297
|
continue;
|
|
1497
|
-
const limited =
|
|
2298
|
+
const limited = await stepped(limiting(batches, replaced, limits, saving.visibleOver));
|
|
1498
2299
|
if (limited !== null)
|
|
1499
2300
|
return { outcome: 'refused', ...limited };
|
|
1500
2301
|
}
|
|
1501
|
-
const
|
|
1502
|
-
|
|
1503
|
-
|
|
2302
|
+
const carried = carriedDrafts(locator, saved, consumed);
|
|
2303
|
+
const left = [...consumed, ...carried.keys()];
|
|
2304
|
+
const arrived = [...carried.values()];
|
|
2305
|
+
const done = await saveCommit(local, files, [
|
|
2306
|
+
{
|
|
1504
2307
|
locator,
|
|
2308
|
+
saving,
|
|
2309
|
+
before: replica,
|
|
1505
2310
|
queue: batches,
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
2311
|
+
write: {
|
|
2312
|
+
locator,
|
|
2313
|
+
queue: batches,
|
|
2314
|
+
saved: drafted,
|
|
2315
|
+
consumedRoomDrafts: left,
|
|
2316
|
+
roomDrafts: arrived,
|
|
2317
|
+
},
|
|
2318
|
+
},
|
|
2319
|
+
]);
|
|
2320
|
+
if (done === null)
|
|
1511
2321
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2322
|
+
const { answer, touched } = done;
|
|
2323
|
+
if (answer.fenced !== null) {
|
|
2324
|
+
throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
|
|
1512
2325
|
}
|
|
1513
2326
|
if (fences.has(locator))
|
|
1514
2327
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1515
2328
|
for (const origin of withdrawn.keys())
|
|
1516
|
-
pendings.delete(origin);
|
|
2329
|
+
pendings.delete(pendingKey(locator, origin));
|
|
1517
2330
|
for (const [id, drafts] of drafted) {
|
|
1518
|
-
pendings.set(
|
|
2331
|
+
pendings.set(pendingKey(locator, id), {
|
|
2332
|
+
drafts,
|
|
2333
|
+
submission: { state: 'waiting' },
|
|
2334
|
+
lost: false,
|
|
2335
|
+
});
|
|
1519
2336
|
}
|
|
1520
|
-
|
|
1521
|
-
recorded = false;
|
|
1522
|
-
for (const path of consumed)
|
|
2337
|
+
for (const path of left)
|
|
1523
2338
|
forgetRoomDraft(locator, path);
|
|
1524
|
-
|
|
1525
|
-
|
|
2339
|
+
for (const record of arrived)
|
|
2340
|
+
rememberRoomDraft(record);
|
|
2341
|
+
savedReplica(locator, replica, batches, touched.get(locator), answer);
|
|
2342
|
+
publishChangeSet(changeSet(locator, {
|
|
2343
|
+
documents: answer.changed[locator] ?? [],
|
|
1526
2344
|
queue: true,
|
|
1527
|
-
drafts:
|
|
2345
|
+
drafts: [...left, ...arrived.map(({ path }) => path)],
|
|
1528
2346
|
}));
|
|
1529
|
-
|
|
1530
|
-
submitLater(locator);
|
|
2347
|
+
queuedSave(locator);
|
|
1531
2348
|
const identifier = join?.batch.id ??
|
|
1532
2349
|
built?.batch.id ??
|
|
1533
2350
|
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
1534
2351
|
return { outcome: 'saved', identifier };
|
|
1535
2352
|
}
|
|
1536
|
-
async function mark(local, locator,
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1539
|
-
state
|
|
1540
|
-
(
|
|
1541
|
-
|
|
2353
|
+
async function mark(local, locator, batch, submission, lost) {
|
|
2354
|
+
const partner = partnerOf(batch, locator);
|
|
2355
|
+
for (const held of partner === null ? [locator] : [locator, partner]) {
|
|
2356
|
+
const state = pendings.get(pendingKey(held, batch.id));
|
|
2357
|
+
if (!continuing() ||
|
|
2358
|
+
state === undefined ||
|
|
2359
|
+
(JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
|
|
2360
|
+
continue;
|
|
2361
|
+
}
|
|
2362
|
+
await local.submit(held, batch.id, submission, lost);
|
|
2363
|
+
if (fences.has(held))
|
|
2364
|
+
continue;
|
|
2365
|
+
pendings.set(pendingKey(held, batch.id), { ...state, submission, lost });
|
|
2366
|
+
if (replicas.has(held))
|
|
2367
|
+
publishChangeSet(changeSet(held, { queue: true }));
|
|
1542
2368
|
}
|
|
1543
|
-
await local.submit(locator, identifier, submission, lost);
|
|
1544
|
-
if (fences.has(locator))
|
|
1545
|
-
return;
|
|
1546
|
-
pendings.set(identifier, { ...state, submission, lost });
|
|
1547
|
-
const replica = replicas.get(locator);
|
|
1548
|
-
if (replica !== undefined)
|
|
1549
|
-
publishChangeSet(changeSet(locator, replica, replica, { queue: true }));
|
|
1550
2369
|
moveTo({ ...status, ...attention() });
|
|
1551
2370
|
}
|
|
1552
2371
|
async function keepUpload(local, locator, identifier, upload) {
|
|
@@ -1555,7 +2374,8 @@ export function composeRuntime(options) {
|
|
|
1555
2374
|
await local.keepUpload(locator, identifier, upload);
|
|
1556
2375
|
if (fences.has(locator))
|
|
1557
2376
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1558
|
-
|
|
2377
|
+
const key = pendingKey(locator, identifier);
|
|
2378
|
+
pendings.set(key, { ...pendings.get(key), upload });
|
|
1559
2379
|
if (!continuing())
|
|
1560
2380
|
throw new Error('connection refused');
|
|
1561
2381
|
}
|
|
@@ -1588,7 +2408,7 @@ export function composeRuntime(options) {
|
|
|
1588
2408
|
if (fencedSince(locator, since))
|
|
1589
2409
|
return failure({ kind: 'room-not-readable', locator });
|
|
1590
2410
|
if (result.kind === 'scoped-refusal') {
|
|
1591
|
-
const room = replicas.get(result.locator)?.
|
|
2411
|
+
const room = replicas.get(result.locator)?.record;
|
|
1592
2412
|
if (room !== undefined)
|
|
1593
2413
|
await scoped(result, room, await openLocal());
|
|
1594
2414
|
}
|
|
@@ -1601,21 +2421,57 @@ export function composeRuntime(options) {
|
|
|
1601
2421
|
return UNCERTAIN;
|
|
1602
2422
|
}
|
|
1603
2423
|
}
|
|
1604
|
-
function
|
|
2424
|
+
function located(room, path, destination) {
|
|
2425
|
+
const crossing = roomTarget(destination);
|
|
2426
|
+
if (crossing === null) {
|
|
2427
|
+
const named = target(path, destination);
|
|
2428
|
+
return named === null || !rooms.has(room) ? null : { room, path: named };
|
|
2429
|
+
}
|
|
2430
|
+
const held = [...rooms.values()].find((listed) => listed.handle === crossing.handle);
|
|
2431
|
+
return held === undefined ? null : { room: held.locator, path: crossing.path };
|
|
2432
|
+
}
|
|
2433
|
+
async function resolveReference(room, path, destination) {
|
|
2434
|
+
if (roomTarget(destination) === null && target(path, destination) === null) {
|
|
2435
|
+
return ABSENT_TARGET;
|
|
2436
|
+
}
|
|
2437
|
+
const at = located(room, path, destination);
|
|
2438
|
+
if (at === null)
|
|
2439
|
+
return UNREADABLE;
|
|
2440
|
+
if (locked(at.room))
|
|
2441
|
+
return LOCKED_UNREADABLE;
|
|
2442
|
+
const held = replicas.get(at.room);
|
|
2443
|
+
if (held !== undefined &&
|
|
2444
|
+
(await (await openLocal()).entries(held.record.key, 'visible', [at.path])).length > 0) {
|
|
2445
|
+
return { kind: 'document', ...at };
|
|
2446
|
+
}
|
|
2447
|
+
const history = await readHistory(at.room, at.path);
|
|
2448
|
+
if (history.kind === 'no-history' || history.kind === 'revision')
|
|
2449
|
+
return ABSENT_TARGET;
|
|
2450
|
+
if (history.kind !== 'history')
|
|
2451
|
+
return history;
|
|
2452
|
+
const [last] = history.revisions;
|
|
2453
|
+
if (last === undefined)
|
|
2454
|
+
return ABSENT_TARGET;
|
|
2455
|
+
return history.deleted === true
|
|
2456
|
+
? { kind: 'deleted', ...at, revision: last }
|
|
2457
|
+
: { kind: 'document', ...at };
|
|
2458
|
+
}
|
|
2459
|
+
function queuedFacts(locator, path, localId) {
|
|
1605
2460
|
for (const batch of replicas.get(locator)?.queue ?? []) {
|
|
1606
2461
|
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
1607
2462
|
if (change?.kind === 'binary')
|
|
1608
|
-
return change
|
|
2463
|
+
return change;
|
|
1609
2464
|
}
|
|
1610
|
-
|
|
2465
|
+
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2466
|
+
return drafted?.localId === localId ? drafted : null;
|
|
1611
2467
|
}
|
|
1612
|
-
function
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
2468
|
+
function batchFile(batch) {
|
|
2469
|
+
return async (localId) => {
|
|
2470
|
+
const change = batch.changes.find((held) => held.kind === 'binary' && held.localId === localId);
|
|
2471
|
+
return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
|
|
2472
|
+
};
|
|
1617
2473
|
}
|
|
1618
|
-
async function
|
|
2474
|
+
async function transferBinary(request, transfer, answer) {
|
|
1619
2475
|
if (closed || refusal !== null)
|
|
1620
2476
|
return UNCERTAIN;
|
|
1621
2477
|
try {
|
|
@@ -1627,73 +2483,109 @@ export function composeRuntime(options) {
|
|
|
1627
2483
|
if (closed || authorization === undefined || refusal !== null)
|
|
1628
2484
|
return UNCERTAIN;
|
|
1629
2485
|
const since = fencings;
|
|
1630
|
-
journal.record(request.
|
|
1631
|
-
? `transfer binary ${request.locator} ${request.path}`
|
|
1632
|
-
: `transfer binary ${request.fileRef}`);
|
|
2486
|
+
journal.record(`transfer binary ${request.locator} ${request.path}`);
|
|
1633
2487
|
try {
|
|
1634
|
-
const result = await
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
if (
|
|
1638
|
-
return failure({ kind: 'room-not-readable', locator });
|
|
1639
|
-
if (
|
|
1640
|
-
const room = replicas.get(
|
|
2488
|
+
const result = await transfer(authorization);
|
|
2489
|
+
const settled = result;
|
|
2490
|
+
journal.record(`outcome binary ${settled.kind}`);
|
|
2491
|
+
if (fencedSince(request.locator, since))
|
|
2492
|
+
return failure({ kind: 'room-not-readable', locator: request.locator });
|
|
2493
|
+
if (settled.kind === 'scoped-refusal') {
|
|
2494
|
+
const room = replicas.get(settled.locator)?.record;
|
|
1641
2495
|
if (room !== undefined)
|
|
1642
|
-
await scoped(
|
|
2496
|
+
await scoped(settled, room, await openLocal());
|
|
1643
2497
|
}
|
|
1644
|
-
if (
|
|
2498
|
+
if (settled.kind === 'revoked-authorization')
|
|
1645
2499
|
await refuse('revoked-authorization');
|
|
1646
|
-
return
|
|
2500
|
+
return answer(result);
|
|
1647
2501
|
}
|
|
1648
2502
|
catch {
|
|
1649
2503
|
journal.record('outcome binary transport-uncertainty');
|
|
1650
2504
|
return UNCERTAIN;
|
|
1651
2505
|
}
|
|
1652
2506
|
}
|
|
1653
|
-
async function
|
|
1654
|
-
|
|
2507
|
+
async function readBinary(request, name) {
|
|
2508
|
+
if (closed)
|
|
2509
|
+
return UNCERTAIN;
|
|
2510
|
+
const held = await keptFiles.find(name);
|
|
2511
|
+
if (held !== null)
|
|
2512
|
+
return held;
|
|
2513
|
+
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
2514
|
+
}
|
|
2515
|
+
async function binaryPath(locator, path, revision) {
|
|
1655
2516
|
if (revision !== undefined)
|
|
1656
|
-
return
|
|
1657
|
-
const visible = replicas.get(locator)?.visible;
|
|
1658
|
-
const held = visible?.binaries[path];
|
|
1659
|
-
if (visible === undefined || held === undefined)
|
|
1660
|
-
return absentBinary(path);
|
|
1661
|
-
if (held.state === 'pending') {
|
|
1662
|
-
const blob = queuedBlob(locator, path, held.localId);
|
|
1663
|
-
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1664
|
-
}
|
|
1665
|
-
const record = binaryRecord(path, held, visible.revisions[path]);
|
|
1666
|
-
if (record.state !== 'accepted')
|
|
1667
|
-
return absentBinary(path);
|
|
2517
|
+
return { kind: 'path', locator, path, revision };
|
|
1668
2518
|
const replica = replicas.get(locator);
|
|
1669
|
-
|
|
2519
|
+
if (replica === undefined)
|
|
2520
|
+
return null;
|
|
2521
|
+
const local = await openLocal();
|
|
2522
|
+
const [held] = await local.entries(replica.record.key, 'visible', [path]);
|
|
2523
|
+
if (held?.binary === undefined || held.binary === null)
|
|
2524
|
+
return null;
|
|
2525
|
+
if (held.binary.state === 'pending')
|
|
2526
|
+
return { kind: 'pending', localId: held.binary.localId };
|
|
2527
|
+
const record = binaryRecord(path, held.binary, held.revision ?? undefined);
|
|
2528
|
+
if (record.state !== 'accepted')
|
|
2529
|
+
return null;
|
|
2530
|
+
const queued = replica.queue.flatMap((batch) => batch.changes);
|
|
2531
|
+
const sources = queued.flatMap((change) => (change.kind === 'move' ? [change.path] : []));
|
|
2532
|
+
const confirmedLayer = await stepped(slicing(await local.entries(replica.record.key, 'confirmed', [path, ...sources]), [
|
|
2533
|
+
path,
|
|
2534
|
+
...sources,
|
|
2535
|
+
]));
|
|
1670
2536
|
const holds = (at) => {
|
|
1671
|
-
const kept =
|
|
2537
|
+
const kept = confirmedLayer.binaries[at];
|
|
1672
2538
|
return (kept?.state === 'accepted' &&
|
|
1673
|
-
kept.
|
|
1674
|
-
|
|
2539
|
+
kept.sha256 === record.sha256 &&
|
|
2540
|
+
confirmedLayer.revisions[at] === record.revision);
|
|
1675
2541
|
};
|
|
1676
|
-
let
|
|
1677
|
-
for (let index = queued.length - 1; index >= 0 && !holds(
|
|
2542
|
+
let confirmedAt = path;
|
|
2543
|
+
for (let index = queued.length - 1; index >= 0 && !holds(confirmedAt); index -= 1) {
|
|
1678
2544
|
const change = queued[index];
|
|
1679
|
-
if (change.kind === 'move' && change.to ===
|
|
1680
|
-
|
|
2545
|
+
if (change.kind === 'move' && change.to === confirmedAt)
|
|
2546
|
+
confirmedAt = change.path;
|
|
1681
2547
|
}
|
|
1682
|
-
return
|
|
2548
|
+
return {
|
|
1683
2549
|
kind: 'path',
|
|
1684
2550
|
locator,
|
|
1685
|
-
path:
|
|
2551
|
+
path: confirmedAt,
|
|
1686
2552
|
revision: record.revision,
|
|
1687
|
-
|
|
1688
|
-
});
|
|
2553
|
+
};
|
|
1689
2554
|
}
|
|
1690
|
-
async function
|
|
2555
|
+
async function readBinaryPath(locator, path, revision) {
|
|
1691
2556
|
await ready;
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
2557
|
+
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2558
|
+
const asked = drafted !== undefined && drafted.localId === revision
|
|
2559
|
+
? { kind: 'pending', localId: revision }
|
|
2560
|
+
: await binaryPath(locator, path, revision);
|
|
2561
|
+
if (asked === null)
|
|
2562
|
+
return absentBinary(path);
|
|
2563
|
+
if (asked.kind === 'pending') {
|
|
2564
|
+
const facts = queuedFacts(locator, path, asked.localId);
|
|
2565
|
+
return facts === null
|
|
2566
|
+
? absentBinary(path)
|
|
2567
|
+
: {
|
|
2568
|
+
kind: 'binary',
|
|
2569
|
+
file: asked.localId,
|
|
2570
|
+
byteLength: facts.byteLength,
|
|
2571
|
+
mediaType: facts.mediaType,
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
2575
|
+
}
|
|
2576
|
+
async function linkBinaryPath(locator, path, revision) {
|
|
2577
|
+
await ready;
|
|
2578
|
+
const asked = await binaryPath(locator, path, revision);
|
|
2579
|
+
if (asked === null || asked.kind === 'pending')
|
|
2580
|
+
return absentBinary(path);
|
|
2581
|
+
return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
|
|
2582
|
+
}
|
|
2583
|
+
function readPart(file, part) {
|
|
2584
|
+
return database.ask({ kind: 'file.part', key: file, part });
|
|
2585
|
+
}
|
|
2586
|
+
async function cleanUp() {
|
|
2587
|
+
const freed = await database.ask({ kind: 'opened.clean' });
|
|
2588
|
+
return freed + ((await options.shell.cleanUp?.()) ?? 0);
|
|
1697
2589
|
}
|
|
1698
2590
|
function declaredAgents() {
|
|
1699
2591
|
for (const capability of capabilities) {
|
|
@@ -1711,7 +2603,14 @@ export function composeRuntime(options) {
|
|
|
1711
2603
|
}
|
|
1712
2604
|
return undefined;
|
|
1713
2605
|
}
|
|
1714
|
-
|
|
2606
|
+
function calledRooms(name, input) {
|
|
2607
|
+
const member = declaredAction(name)?.room;
|
|
2608
|
+
if (member === undefined || input === null || typeof input !== 'object')
|
|
2609
|
+
return [];
|
|
2610
|
+
const named = input[member];
|
|
2611
|
+
return (Array.isArray(named) ? named : [named]).filter((room) => typeof room === 'string');
|
|
2612
|
+
}
|
|
2613
|
+
async function invokeAction(name, input, dispatching) {
|
|
1715
2614
|
await ready;
|
|
1716
2615
|
if (closed || refusal !== null)
|
|
1717
2616
|
return UNCERTAIN;
|
|
@@ -1725,11 +2624,12 @@ export function composeRuntime(options) {
|
|
|
1725
2624
|
}
|
|
1726
2625
|
if (closed || authorization === undefined || refusal !== null)
|
|
1727
2626
|
return UNCERTAIN;
|
|
2627
|
+
await dispatching?.();
|
|
1728
2628
|
journal.record(`transfer action ${name}`);
|
|
1729
2629
|
try {
|
|
1730
2630
|
const result = await options.adapter.action(authorization, { name, input });
|
|
1731
2631
|
if (result.kind === 'scoped-refusal') {
|
|
1732
|
-
const room = replicas.get(result.locator)?.
|
|
2632
|
+
const room = replicas.get(result.locator)?.record;
|
|
1733
2633
|
if (room !== undefined)
|
|
1734
2634
|
await scoped(result, room, await openLocal());
|
|
1735
2635
|
}
|
|
@@ -1745,107 +2645,515 @@ export function composeRuntime(options) {
|
|
|
1745
2645
|
return UNCERTAIN;
|
|
1746
2646
|
}
|
|
1747
2647
|
}
|
|
2648
|
+
function unsettled(result) {
|
|
2649
|
+
return result.kind !== 'action' && result.retryable;
|
|
2650
|
+
}
|
|
2651
|
+
async function recoveredAction(name, input) {
|
|
2652
|
+
const operation = operationOf(input);
|
|
2653
|
+
if (operation === null)
|
|
2654
|
+
return invokeAction(name, input);
|
|
2655
|
+
const kept = async () => (await openLocal()).keepCall({ operation, name, input });
|
|
2656
|
+
calling.add(operation);
|
|
2657
|
+
try {
|
|
2658
|
+
let result = await invokeAction(name, input, kept);
|
|
2659
|
+
if (unsettled(result)) {
|
|
2660
|
+
await recovery.attempt(`action ${operation}`, async () => {
|
|
2661
|
+
if (closed || refusal !== null)
|
|
2662
|
+
return 'done';
|
|
2663
|
+
if (!socketOpen)
|
|
2664
|
+
return 'again';
|
|
2665
|
+
result = await invokeAction(name, input, kept);
|
|
2666
|
+
return unsettled(result) ? 'again' : 'done';
|
|
2667
|
+
}, { again: true });
|
|
2668
|
+
}
|
|
2669
|
+
if (!unsettled(result))
|
|
2670
|
+
await (await openLocal()).dropCall(operation);
|
|
2671
|
+
return result;
|
|
2672
|
+
}
|
|
2673
|
+
finally {
|
|
2674
|
+
calling.delete(operation);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
async function resendCalls(signal) {
|
|
2678
|
+
if (!socketOpen || closed || stopping)
|
|
2679
|
+
return 'done';
|
|
2680
|
+
const local = await openLocal();
|
|
2681
|
+
for (const held of await local.calls()) {
|
|
2682
|
+
if (calling.has(held.operation))
|
|
2683
|
+
continue;
|
|
2684
|
+
if (closed || signal.aborted)
|
|
2685
|
+
return 'done';
|
|
2686
|
+
if (pastHorizon(held.operation)) {
|
|
2687
|
+
await local.dropCall(held.operation);
|
|
2688
|
+
continue;
|
|
2689
|
+
}
|
|
2690
|
+
calling.add(held.operation);
|
|
2691
|
+
try {
|
|
2692
|
+
if (unsettled(await invokeAction(held.name, held.input)))
|
|
2693
|
+
return 'again';
|
|
2694
|
+
await local.dropCall(held.operation);
|
|
2695
|
+
}
|
|
2696
|
+
finally {
|
|
2697
|
+
calling.delete(held.operation);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
return 'done';
|
|
2701
|
+
}
|
|
1748
2702
|
function continuing() {
|
|
1749
2703
|
return !closed && !discoveryPaused;
|
|
1750
2704
|
}
|
|
1751
2705
|
function reachedBy(locator, roomVersion) {
|
|
1752
2706
|
const held = replicas.get(locator);
|
|
1753
|
-
return (held !== undefined &&
|
|
1754
|
-
|
|
2707
|
+
return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
|
|
2708
|
+
}
|
|
2709
|
+
async function writtenAfter(locator, roomVersion, path) {
|
|
2710
|
+
const held = replicas.get(locator);
|
|
2711
|
+
if (held === undefined)
|
|
2712
|
+
return false;
|
|
2713
|
+
const [entry] = await (await openLocal()).entries(held.record.key, 'confirmed', [path]);
|
|
2714
|
+
const written = entry?.written ?? undefined;
|
|
2715
|
+
return (written !== undefined &&
|
|
2716
|
+
options.adapter.reached(locator, written, roomVersion) &&
|
|
2717
|
+
!options.adapter.reached(locator, roomVersion, written));
|
|
2718
|
+
}
|
|
2719
|
+
function overriddenTexts(texts, overrides) {
|
|
2720
|
+
return async (paths) => {
|
|
2721
|
+
const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
|
|
2722
|
+
for (const path of paths) {
|
|
2723
|
+
const entry = overrides.get(path);
|
|
2724
|
+
if (entry === undefined)
|
|
2725
|
+
continue;
|
|
2726
|
+
if (entry === null)
|
|
2727
|
+
known.delete(path);
|
|
2728
|
+
else
|
|
2729
|
+
known.set(path, entry.binary !== null ? null : entry.content);
|
|
2730
|
+
}
|
|
2731
|
+
return known;
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
async function unreadableRoom(texts, walk, paths) {
|
|
2735
|
+
if (paths !== null) {
|
|
2736
|
+
const text = {};
|
|
2737
|
+
for (const [path, content] of await texts(paths))
|
|
2738
|
+
if (content !== null)
|
|
2739
|
+
text[path] = content;
|
|
2740
|
+
return stepped(unreadableIn(text, Object.keys(text)));
|
|
2741
|
+
}
|
|
2742
|
+
for (let after = null;;) {
|
|
2743
|
+
const page = await walk(after);
|
|
2744
|
+
const text = {};
|
|
2745
|
+
for (const { path, content } of page.entries)
|
|
2746
|
+
if (content !== null)
|
|
2747
|
+
text[path] = content;
|
|
2748
|
+
const refused = await stepped(unreadableIn(text, Object.keys(text)));
|
|
2749
|
+
if (refused !== null)
|
|
2750
|
+
return refused;
|
|
2751
|
+
if (page.next === null)
|
|
2752
|
+
return null;
|
|
2753
|
+
after = page.next;
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
async function validatedRoom(local, record, view, previous, overrides = null) {
|
|
2757
|
+
const composition = compositionOf(record);
|
|
2758
|
+
const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
|
|
2759
|
+
const lookup = overrides === null
|
|
2760
|
+
? lookupOf(local, record.key, view)
|
|
2761
|
+
: overriddenLookup(lookupOf(local, record.key, view), overrides);
|
|
2762
|
+
const texts = overrides === null
|
|
2763
|
+
? textsOf(local, record.key, view)
|
|
2764
|
+
: overriddenTexts(textsOf(local, record.key, view), overrides);
|
|
2765
|
+
const walk = overrides === null
|
|
2766
|
+
? walkOf(local, record.key, view)
|
|
2767
|
+
: overriddenWalk(walkOf(local, record.key, view), overrides);
|
|
2768
|
+
let changes = null;
|
|
2769
|
+
let reshaped = false;
|
|
2770
|
+
if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
|
|
2771
|
+
const [was, now] = await Promise.all([
|
|
2772
|
+
local.entries(record.key, 'confirmed', previous.changed),
|
|
2773
|
+
overrides === null
|
|
2774
|
+
? local.entries(record.key, view, previous.changed)
|
|
2775
|
+
: Promise.resolve([]),
|
|
2776
|
+
]);
|
|
2777
|
+
const before = new Map(was.map((entry) => [entry.path, entry]));
|
|
2778
|
+
const after = overrides === null
|
|
2779
|
+
? new Map(now.map((entry) => [entry.path, entry]))
|
|
2780
|
+
: overrides;
|
|
2781
|
+
changes = previous.changed.flatMap((path) => {
|
|
2782
|
+
const one = before.get(path) ?? null;
|
|
2783
|
+
const other = after.get(path) ?? null;
|
|
2784
|
+
reshaped ||= textDiffers(one, other);
|
|
2785
|
+
const held = textOf(one);
|
|
2786
|
+
const next = textOf(other);
|
|
2787
|
+
return held === next ? [] : [{ path, was: held, now: next }];
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
const named = changes === null ? null : changes.map(({ path }) => path);
|
|
2791
|
+
const textChanged = changes === null || changes.length > 0 || reshaped;
|
|
2792
|
+
const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
|
|
2793
|
+
if (modeled && (textChanged || modelsChanged)) {
|
|
2794
|
+
const clean = previous !== null &&
|
|
2795
|
+
changes !== null &&
|
|
2796
|
+
(await changedCleanly(changes, compositionOf(previous.record), composition, {
|
|
2797
|
+
lookup,
|
|
2798
|
+
texts,
|
|
2799
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2800
|
+
}));
|
|
2801
|
+
if (clean)
|
|
2802
|
+
return null;
|
|
2803
|
+
const verdict = await validatedCollection(walk, lookup, composition);
|
|
2804
|
+
if (verdict.verdict === 'accept')
|
|
2805
|
+
return null;
|
|
2806
|
+
if (verdict.rule !== 'model-unavailable')
|
|
2807
|
+
return { kind: 'refused', refusal: verdict };
|
|
2808
|
+
return unreadableRoom(texts, walk, named);
|
|
2809
|
+
}
|
|
2810
|
+
if (textChanged)
|
|
2811
|
+
return unreadableRoom(texts, walk, named);
|
|
2812
|
+
return null;
|
|
2813
|
+
}
|
|
2814
|
+
function refreshingOf(local, key, liveness, replaying = null) {
|
|
2815
|
+
const write = replaying === null ? exclusive : installWrite;
|
|
2816
|
+
return {
|
|
2817
|
+
async turn() {
|
|
2818
|
+
if (replaying === null)
|
|
2819
|
+
return;
|
|
2820
|
+
installing(replaying, 'replaying');
|
|
2821
|
+
await screened();
|
|
2822
|
+
},
|
|
2823
|
+
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2824
|
+
async stage(page) {
|
|
2825
|
+
for (let written = 0;;) {
|
|
2826
|
+
if (replaying !== null)
|
|
2827
|
+
await screened();
|
|
2828
|
+
const end = batchEnd(page.entries, written);
|
|
2829
|
+
const last = end === page.entries.length;
|
|
2830
|
+
const entries = page.entries.slice(written, end);
|
|
2831
|
+
const staged = await write(async () => {
|
|
2832
|
+
if (!liveness())
|
|
2833
|
+
return false;
|
|
2834
|
+
await timed(() => local.refresh(key, last
|
|
2835
|
+
? { ...page, entries }
|
|
2836
|
+
: { entries, removals: [], folders: [], folderRemovals: [] }));
|
|
2837
|
+
return true;
|
|
2838
|
+
});
|
|
2839
|
+
if (!staged)
|
|
2840
|
+
return false;
|
|
2841
|
+
if (last)
|
|
2842
|
+
return true;
|
|
2843
|
+
written = end;
|
|
2844
|
+
}
|
|
2845
|
+
},
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
function batchEnd(entries, from) {
|
|
2849
|
+
let end = from;
|
|
2850
|
+
let spent = 0;
|
|
2851
|
+
while (end < entries.length) {
|
|
2852
|
+
const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
|
|
2853
|
+
if (spent + size > stageBytes && end > from)
|
|
2854
|
+
break;
|
|
2855
|
+
spent += size;
|
|
2856
|
+
end += 1;
|
|
2857
|
+
}
|
|
2858
|
+
return end;
|
|
2859
|
+
}
|
|
2860
|
+
async function timed(work) {
|
|
2861
|
+
const started = performance.now();
|
|
2862
|
+
const done = await work();
|
|
2863
|
+
const spent = performance.now() - started;
|
|
2864
|
+
if (spent < STAGE_AIM_MS / 2)
|
|
2865
|
+
stageBytes = Math.min(STAGE_BYTES, Math.round(stageBytes * 1.5));
|
|
2866
|
+
else if (spent > STAGE_AIM_MS * 2) {
|
|
2867
|
+
stageBytes = Math.max(STAGE_FLOOR, Math.round(stageBytes / 1.5));
|
|
2868
|
+
}
|
|
2869
|
+
return done;
|
|
2870
|
+
}
|
|
2871
|
+
function installWrite(work) {
|
|
2872
|
+
const next = installWrites.then(() => exclusive(work), () => exclusive(work));
|
|
2873
|
+
installWrites = next.then(() => undefined, () => undefined);
|
|
2874
|
+
return next;
|
|
2875
|
+
}
|
|
2876
|
+
function screened() {
|
|
2877
|
+
if (onScreen || closed)
|
|
2878
|
+
return Promise.resolve();
|
|
2879
|
+
queueReadiness();
|
|
2880
|
+
return new Promise((resolve) => shownWaiters.push(resolve));
|
|
2881
|
+
}
|
|
2882
|
+
async function heldPart() {
|
|
2883
|
+
while (partsFree === 0)
|
|
2884
|
+
await new Promise((admit) => partWaiters.push(admit));
|
|
2885
|
+
partsFree -= 1;
|
|
2886
|
+
return freed();
|
|
2887
|
+
}
|
|
2888
|
+
function freed() {
|
|
2889
|
+
let released = false;
|
|
2890
|
+
return () => {
|
|
2891
|
+
if (released)
|
|
2892
|
+
return;
|
|
2893
|
+
released = true;
|
|
2894
|
+
partsFree += 1;
|
|
2895
|
+
partWaiters.shift()?.();
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2898
|
+
function installing(locator, step, share = null) {
|
|
2899
|
+
installs.set(locator, { step, share });
|
|
2900
|
+
queueReadiness();
|
|
1755
2901
|
}
|
|
1756
|
-
function
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
options.adapter.reached(locator, written, roomVersion) &&
|
|
1760
|
-
!options.adapter.reached(locator, roomVersion, written));
|
|
2902
|
+
function installed(locator) {
|
|
2903
|
+
if (installs.delete(locator))
|
|
2904
|
+
queueReadiness();
|
|
1761
2905
|
}
|
|
1762
|
-
|
|
1763
|
-
if (
|
|
2906
|
+
function screen(value) {
|
|
2907
|
+
if (value === onScreen)
|
|
2908
|
+
return;
|
|
2909
|
+
onScreen = value;
|
|
2910
|
+
journal.record(`installation ${value ? 'shown' : 'hidden'}`);
|
|
2911
|
+
queueReadiness();
|
|
2912
|
+
if (!value)
|
|
2913
|
+
return;
|
|
2914
|
+
for (const resolve of shownWaiters.splice(0))
|
|
2915
|
+
resolve();
|
|
2916
|
+
}
|
|
2917
|
+
async function pullRefresh(authorized, held, liveness) {
|
|
2918
|
+
if (!liveness())
|
|
1764
2919
|
return unconfirmed;
|
|
2920
|
+
const turn = generation;
|
|
2921
|
+
const { record } = held;
|
|
2922
|
+
const base = held.confirmed;
|
|
2923
|
+
const local = await openLocal();
|
|
1765
2924
|
try {
|
|
1766
|
-
|
|
1767
|
-
const
|
|
1768
|
-
|
|
1769
|
-
|
|
2925
|
+
await exclusive(() => local.unrefresh(record.key));
|
|
2926
|
+
const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, liveness));
|
|
2927
|
+
if (result.kind !== 'advanced') {
|
|
2928
|
+
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2929
|
+
return result;
|
|
2930
|
+
}
|
|
2931
|
+
const refused = await validatedRoom(local, result.record, 'refreshed', {
|
|
2932
|
+
record,
|
|
2933
|
+
changed: [...result.changed],
|
|
2934
|
+
});
|
|
2935
|
+
if (refused !== null) {
|
|
2936
|
+
journal.record(`transfer refresh ${record.locator} refused`);
|
|
2937
|
+
return refused;
|
|
2938
|
+
}
|
|
2939
|
+
journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
|
|
2940
|
+
return { kind: 'advanced', record: result.record, base };
|
|
1770
2941
|
}
|
|
1771
2942
|
catch (error) {
|
|
1772
|
-
|
|
2943
|
+
if (refusedForSpace(error))
|
|
2944
|
+
return unconfirmed;
|
|
2945
|
+
journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
|
|
1773
2946
|
throw error;
|
|
1774
2947
|
}
|
|
2948
|
+
finally {
|
|
2949
|
+
reads.finished(record.locator, turn);
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
const heldContents = new Map();
|
|
2953
|
+
function heldContentOf(local, key) {
|
|
2954
|
+
const known = heldContents.get(key);
|
|
2955
|
+
if (known !== undefined)
|
|
2956
|
+
return known;
|
|
2957
|
+
if (heldContents.size > ROOM_READS)
|
|
2958
|
+
heldContents.clear();
|
|
2959
|
+
const made = {
|
|
2960
|
+
files: (after, limit) => local.files(key, 'confirmed', after, limit),
|
|
2961
|
+
async contents(paths) {
|
|
2962
|
+
return new Map((await local.entries(key, 'confirmed', paths)).flatMap(({ path, content, revision }) => content === null || revision === null ? [] : [[path, { content, revision }]]));
|
|
2963
|
+
},
|
|
2964
|
+
};
|
|
2965
|
+
heldContents.set(key, made);
|
|
2966
|
+
return made;
|
|
1775
2967
|
}
|
|
1776
|
-
function staging(local,
|
|
2968
|
+
function staging(local, liveness) {
|
|
1777
2969
|
return {
|
|
1778
2970
|
held: (locator) => transfers.get(locator) ?? null,
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
await local.stage(record, page);
|
|
2971
|
+
async turn(locator, step) {
|
|
2972
|
+
installing(locator, step);
|
|
2973
|
+
for (;;) {
|
|
2974
|
+
await screened();
|
|
2975
|
+
const release = step === 'downloading' ? await heldPart() : () => { };
|
|
2976
|
+
if (onScreen || closed)
|
|
2977
|
+
return release;
|
|
2978
|
+
release();
|
|
1788
2979
|
}
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
2980
|
+
},
|
|
2981
|
+
spare() {
|
|
2982
|
+
if (!onScreen || closed || partsFree === 0)
|
|
2983
|
+
return null;
|
|
2984
|
+
partsFree -= 1;
|
|
2985
|
+
return freed();
|
|
2986
|
+
},
|
|
2987
|
+
arriving(locator) {
|
|
2988
|
+
let ended = false;
|
|
2989
|
+
return {
|
|
2990
|
+
at(token, bytes) {
|
|
2991
|
+
if (ended)
|
|
2992
|
+
return;
|
|
2993
|
+
const held = arrivals.get(locator) ?? new Map();
|
|
2994
|
+
arrivals.set(locator, held.set(token, bytes));
|
|
2995
|
+
const now = performance.now();
|
|
2996
|
+
if (now - arrivalReported < ARRIVAL_REPORT_MS)
|
|
2997
|
+
return;
|
|
2998
|
+
arrivalReported = now;
|
|
2999
|
+
queueReadiness();
|
|
3000
|
+
},
|
|
3001
|
+
end() {
|
|
3002
|
+
ended = true;
|
|
3003
|
+
if (arrivals.delete(locator))
|
|
3004
|
+
queueReadiness();
|
|
3005
|
+
},
|
|
3006
|
+
};
|
|
3007
|
+
},
|
|
3008
|
+
received: (locator, token) => local.transferPart(locator, token),
|
|
3009
|
+
keep: (locator, token, received) => installWrite(async () => {
|
|
3010
|
+
if (liveness())
|
|
3011
|
+
await local.keepPart(locator, token, received);
|
|
1798
3012
|
}),
|
|
3013
|
+
announced(locator) {
|
|
3014
|
+
const later = announced.get(locator);
|
|
3015
|
+
return later === undefined ? [] : [later];
|
|
3016
|
+
},
|
|
3017
|
+
async stage(record, page, composition, received) {
|
|
3018
|
+
const { locator } = record;
|
|
3019
|
+
const whole = received?.byteLength ?? 0;
|
|
3020
|
+
const from = record.writing?.written ?? 0;
|
|
3021
|
+
const token = record.writing?.token ?? null;
|
|
3022
|
+
const entries = page.entries.length;
|
|
3023
|
+
let held = record;
|
|
3024
|
+
if (token !== null)
|
|
3025
|
+
arrivals.get(locator)?.delete(token);
|
|
3026
|
+
installing(locator, 'writing', { written: from, entries, bytes: whole });
|
|
3027
|
+
for (let written = from;;) {
|
|
3028
|
+
await screened();
|
|
3029
|
+
if (!liveness())
|
|
3030
|
+
return null;
|
|
3031
|
+
const end = batchEnd(page.entries, written);
|
|
3032
|
+
const last = end === entries;
|
|
3033
|
+
const next = last
|
|
3034
|
+
? {
|
|
3035
|
+
...held,
|
|
3036
|
+
writing: null,
|
|
3037
|
+
done: {
|
|
3038
|
+
parts: held.done.parts + (received === null ? 0 : 1),
|
|
3039
|
+
bytes: held.done.bytes + whole,
|
|
3040
|
+
},
|
|
3041
|
+
}
|
|
3042
|
+
: { ...held, writing: { token, written: end } };
|
|
3043
|
+
const key = await installWrite(async () => {
|
|
3044
|
+
if (!liveness())
|
|
3045
|
+
return null;
|
|
3046
|
+
return timed(() => local.stage(next, composition, {
|
|
3047
|
+
entries: page.entries.slice(written, end),
|
|
3048
|
+
folders: last ? page.folders : [],
|
|
3049
|
+
}, last && received !== null && token !== null ? token : undefined));
|
|
3050
|
+
});
|
|
3051
|
+
if (key === null)
|
|
3052
|
+
return null;
|
|
3053
|
+
const arrived = !transfers.has(locator);
|
|
3054
|
+
held = { ...next, key };
|
|
3055
|
+
transfers.set(locator, held);
|
|
3056
|
+
if (arrived)
|
|
3057
|
+
liveQueries.recover();
|
|
3058
|
+
if (last) {
|
|
3059
|
+
installing(locator, held.pageToken === null ? 'replaying' : 'downloading');
|
|
3060
|
+
liveQueries.recover();
|
|
3061
|
+
return held;
|
|
3062
|
+
}
|
|
3063
|
+
installing(locator, 'writing', { written: end, entries, bytes: whole });
|
|
3064
|
+
written = end;
|
|
3065
|
+
}
|
|
3066
|
+
},
|
|
3067
|
+
async refreshing(locator, key) {
|
|
3068
|
+
await exclusive(() => local.unrefresh(key));
|
|
3069
|
+
return refreshingOf(local, key, liveness, locator);
|
|
3070
|
+
},
|
|
1799
3071
|
abandon: (locator) => exclusive(async () => {
|
|
1800
|
-
if (!
|
|
3072
|
+
if (!liveness())
|
|
1801
3073
|
return;
|
|
1802
3074
|
await local.abandon(locator);
|
|
1803
3075
|
transfers.delete(locator);
|
|
1804
|
-
|
|
3076
|
+
liveQueries.recover();
|
|
3077
|
+
sweep();
|
|
3078
|
+
}),
|
|
3079
|
+
setAside: (locator) => exclusive(async () => {
|
|
3080
|
+
if (!liveness())
|
|
3081
|
+
return;
|
|
3082
|
+
await local.abandon(locator, true);
|
|
3083
|
+
transfers.delete(locator);
|
|
3084
|
+
liveQueries.recover();
|
|
1805
3085
|
}),
|
|
3086
|
+
release: (key) => exclusive(async () => {
|
|
3087
|
+
await local.release(key);
|
|
3088
|
+
sweep();
|
|
3089
|
+
}),
|
|
3090
|
+
content: (key) => heldContentOf(local, key),
|
|
3091
|
+
validate: (record, view) => validatedRoom(local, record, view, null),
|
|
1806
3092
|
};
|
|
1807
3093
|
}
|
|
1808
|
-
function reusable(room) {
|
|
1809
|
-
const record = replicas.get(room.locator)?.
|
|
3094
|
+
function reusable(room, local) {
|
|
3095
|
+
const record = replicas.get(room.locator)?.record;
|
|
1810
3096
|
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1811
3097
|
return null;
|
|
1812
|
-
return
|
|
3098
|
+
return heldContentOf(local, record.key);
|
|
1813
3099
|
}
|
|
1814
|
-
async function pullBootstrap(authorized, room, local,
|
|
1815
|
-
if (!
|
|
3100
|
+
async function pullBootstrap(authorized, room, local, liveness) {
|
|
3101
|
+
if (!liveness())
|
|
1816
3102
|
return unconfirmed;
|
|
3103
|
+
const turn = generation;
|
|
1817
3104
|
if (replicas.has(room.locator)) {
|
|
1818
3105
|
catching.add(room.locator);
|
|
1819
|
-
|
|
3106
|
+
liveQueries.recover();
|
|
1820
3107
|
}
|
|
1821
3108
|
try {
|
|
1822
|
-
const result = await bootstrapRoom(
|
|
3109
|
+
const result = await bootstrapRoom(reads, authorized, room, {
|
|
1823
3110
|
collection: modelOf(room.locator),
|
|
1824
3111
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1825
|
-
}, (models, held) => (
|
|
1826
|
-
const detail = result.kind === 'staged' ? result.
|
|
3112
|
+
}, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
|
|
3113
|
+
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
1827
3114
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1828
|
-
if (result.kind !== 'staged')
|
|
3115
|
+
if (result.kind !== 'staged') {
|
|
1829
3116
|
caughtUpTo(room.locator);
|
|
3117
|
+
installed(room.locator);
|
|
3118
|
+
}
|
|
1830
3119
|
return result;
|
|
1831
3120
|
}
|
|
1832
3121
|
catch (error) {
|
|
1833
|
-
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1834
3122
|
caughtUpTo(room.locator);
|
|
3123
|
+
installed(room.locator);
|
|
3124
|
+
if (refusedForSpace(error))
|
|
3125
|
+
return unconfirmed;
|
|
3126
|
+
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1835
3127
|
throw error;
|
|
1836
3128
|
}
|
|
3129
|
+
finally {
|
|
3130
|
+
reads.finished(room.locator, turn);
|
|
3131
|
+
}
|
|
1837
3132
|
}
|
|
1838
3133
|
function caughtUpTo(locator) {
|
|
1839
3134
|
if (catching.delete(locator))
|
|
1840
|
-
|
|
3135
|
+
liveQueries.recover();
|
|
3136
|
+
}
|
|
3137
|
+
function behind(room) {
|
|
3138
|
+
const held = replicas.get(room.locator)?.record.roomVersion;
|
|
3139
|
+
return (held !== undefined &&
|
|
3140
|
+
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
1841
3141
|
}
|
|
1842
3142
|
async function advance(room, local, authorized, live) {
|
|
1843
|
-
|
|
1844
|
-
|
|
3143
|
+
const carried = reads.carried(room.locator);
|
|
3144
|
+
const withheld = await advanceOnce(room, local, authorized, live);
|
|
3145
|
+
if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
|
|
3146
|
+
return withheld;
|
|
3147
|
+
}
|
|
3148
|
+
return advanceOnce(room, local, authorized, live);
|
|
3149
|
+
}
|
|
3150
|
+
async function advanceOnce(room, local, authorized, running) {
|
|
3151
|
+
const live = () => running() && !full;
|
|
3152
|
+
if (!live())
|
|
1845
3153
|
return unconfirmed;
|
|
1846
3154
|
const held = replicas.get(room.locator);
|
|
1847
3155
|
if (held !== undefined) {
|
|
1848
|
-
const refreshed = await pullRefresh(authorized, held
|
|
3156
|
+
const refreshed = await pullRefresh(authorized, held, live);
|
|
1849
3157
|
if (!live())
|
|
1850
3158
|
return unconfirmed;
|
|
1851
3159
|
if (refreshed.kind === 'still-current') {
|
|
@@ -1860,10 +3168,11 @@ export function composeRuntime(options) {
|
|
|
1860
3168
|
return null;
|
|
1861
3169
|
}
|
|
1862
3170
|
if (refreshed.kind === 'advanced') {
|
|
1863
|
-
|
|
1864
|
-
|
|
3171
|
+
const landing = { kind: 'refreshed', base: refreshed.base };
|
|
3172
|
+
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3173
|
+
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
1865
3174
|
const reached = now !== undefined &&
|
|
1866
|
-
options.adapter.reached(room.locator, now, refreshed.
|
|
3175
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
1867
3176
|
return reached ? null : advance(room, local, authorized, live);
|
|
1868
3177
|
}
|
|
1869
3178
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
@@ -1873,17 +3182,18 @@ export function composeRuntime(options) {
|
|
|
1873
3182
|
return refreshed;
|
|
1874
3183
|
if (opened !== null) {
|
|
1875
3184
|
owed.add(room.locator);
|
|
1876
|
-
|
|
3185
|
+
liveQueries.recover();
|
|
1877
3186
|
return unconfirmed;
|
|
1878
3187
|
}
|
|
3188
|
+
if (reloadsDue.get(room.locator) === generation)
|
|
3189
|
+
return unconfirmed;
|
|
1879
3190
|
}
|
|
1880
3191
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1881
|
-
if (
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
return null;
|
|
3192
|
+
if (bootstrapped.kind === 'staged') {
|
|
3193
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
3194
|
+
return live() ? null : unconfirmed;
|
|
3195
|
+
}
|
|
3196
|
+
return live() ? bootstrapped : unconfirmed;
|
|
1887
3197
|
}
|
|
1888
3198
|
async function withhold(withheld, room, local, progress) {
|
|
1889
3199
|
if (withheld.kind === 'unconfirmed')
|
|
@@ -1955,57 +3265,254 @@ export function composeRuntime(options) {
|
|
|
1955
3265
|
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
1956
3266
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1957
3267
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
3268
|
+
function settling(acceptance) {
|
|
3269
|
+
return acceptance !== null && 'apply' in acceptance;
|
|
3270
|
+
}
|
|
3271
|
+
function refusalOf(acceptance) {
|
|
3272
|
+
return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
|
|
3273
|
+
}
|
|
3274
|
+
async function acceptance(room, local, submitted, accepted) {
|
|
1960
3275
|
if (!continuing())
|
|
1961
3276
|
return unconfirmed;
|
|
1962
3277
|
const current = replicas.get(room.locator);
|
|
1963
3278
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1964
3279
|
return null;
|
|
1965
3280
|
}
|
|
1966
|
-
const held = current.
|
|
3281
|
+
const held = current.record.roomVersion;
|
|
1967
3282
|
logAccepted(room.locator, submitted, accepted);
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
3283
|
+
let record = current.record;
|
|
3284
|
+
let changes;
|
|
3285
|
+
if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
|
|
3286
|
+
const paths = [...returnedPaths(submitted, accepted)];
|
|
3287
|
+
const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
|
|
3288
|
+
const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
|
|
3289
|
+
record = {
|
|
3290
|
+
...record,
|
|
3291
|
+
roomVersion: options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
3292
|
+
? accepted.roomVersion
|
|
3293
|
+
: held,
|
|
3294
|
+
};
|
|
3295
|
+
const overrides = new Map([
|
|
3296
|
+
...back.set.map((entry) => [entry.path, entry]),
|
|
3297
|
+
...back.removed.map((path) => [path, null]),
|
|
3298
|
+
]);
|
|
3299
|
+
const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
|
|
3300
|
+
if (refused !== null) {
|
|
3301
|
+
const key = pendingKey(room.locator, submitted.id);
|
|
3302
|
+
return {
|
|
3303
|
+
locator: room.locator,
|
|
3304
|
+
left: [],
|
|
3305
|
+
commit: {
|
|
3306
|
+
rooms: [],
|
|
3307
|
+
writes: [
|
|
3308
|
+
{
|
|
3309
|
+
locator: room.locator,
|
|
3310
|
+
accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
|
|
3311
|
+
},
|
|
3312
|
+
],
|
|
3313
|
+
},
|
|
3314
|
+
refused,
|
|
3315
|
+
apply() {
|
|
3316
|
+
const state = pendings.get(key);
|
|
3317
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
3318
|
+
pendings.set(key, { ...state, accepted: accepted.roomVersion });
|
|
3319
|
+
return changeSet(room.locator, { queue: true });
|
|
3320
|
+
},
|
|
3321
|
+
};
|
|
3322
|
+
}
|
|
3323
|
+
changes = back;
|
|
3324
|
+
}
|
|
3325
|
+
const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
3326
|
+
return {
|
|
3327
|
+
...settled,
|
|
3328
|
+
apply(answer) {
|
|
3329
|
+
const set = settled.apply(answer);
|
|
3330
|
+
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3331
|
+
unreadable.delete(room.locator);
|
|
3332
|
+
sourceRefusals.delete(room.locator);
|
|
3333
|
+
declarationRefusals.delete(room.locator);
|
|
3334
|
+
list(room, true);
|
|
3335
|
+
return set;
|
|
3336
|
+
},
|
|
3337
|
+
};
|
|
3338
|
+
}
|
|
3339
|
+
async function adoptedTogether(local, acceptances) {
|
|
3340
|
+
const settlements = acceptances.filter(settling);
|
|
3341
|
+
if (settlements.length > 0) {
|
|
3342
|
+
publishChangeSet(await together(local, settlements));
|
|
3343
|
+
moveTo({ ...status, ...attention() });
|
|
3344
|
+
}
|
|
3345
|
+
await unpair(local);
|
|
1991
3346
|
}
|
|
1992
3347
|
async function adopted(room, local, submitted, accepted) {
|
|
1993
|
-
const failed = await exclusive(() =>
|
|
3348
|
+
const failed = await exclusive(async () => {
|
|
3349
|
+
const adopting = await acceptance(room, local, submitted, accepted);
|
|
3350
|
+
await adoptedTogether(local, [adopting]);
|
|
3351
|
+
return refusalOf(adopting);
|
|
3352
|
+
});
|
|
3353
|
+
if (failed === null)
|
|
3354
|
+
return true;
|
|
3355
|
+
if (failed.kind === 'refused' && continuing())
|
|
3356
|
+
refusedSource(room, failed);
|
|
3357
|
+
return false;
|
|
3358
|
+
}
|
|
3359
|
+
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
3360
|
+
const other = discovered.get(partner);
|
|
3361
|
+
const failed = await exclusive(async () => {
|
|
3362
|
+
const left = await acceptance(room, local, submitted, accepted);
|
|
3363
|
+
const arriving = replicas.get(partner)?.queue.find((batch) => batch.id === submitted.id);
|
|
3364
|
+
const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
|
|
3365
|
+
? null
|
|
3366
|
+
: await acceptance(other, local, arriving, {
|
|
3367
|
+
kind: 'accepted',
|
|
3368
|
+
...accepted.destination,
|
|
3369
|
+
});
|
|
3370
|
+
await adoptedTogether(local, [left, arrived]);
|
|
3371
|
+
const refusedArrival = refusalOf(arrived);
|
|
3372
|
+
if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
|
|
3373
|
+
refusedSource(other, refusedArrival);
|
|
3374
|
+
}
|
|
3375
|
+
return refusalOf(left);
|
|
3376
|
+
});
|
|
3377
|
+
submitLater(partner);
|
|
1994
3378
|
if (failed === null)
|
|
1995
3379
|
return true;
|
|
1996
3380
|
if (failed.kind === 'refused' && continuing())
|
|
1997
3381
|
refusedSource(room, failed);
|
|
1998
3382
|
return false;
|
|
1999
3383
|
}
|
|
3384
|
+
function queueIn(locator) {
|
|
3385
|
+
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
3386
|
+
}
|
|
3387
|
+
function behindIn(locator, batch) {
|
|
3388
|
+
const held = replicas.get(locator)?.queue;
|
|
3389
|
+
if (held === undefined)
|
|
3390
|
+
return queuesWithoutReplica.get(locator) !== undefined;
|
|
3391
|
+
return held.findIndex((one) => one.id === batch.id) > 0;
|
|
3392
|
+
}
|
|
3393
|
+
function unpaired(skipped) {
|
|
3394
|
+
for (const locator of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
3395
|
+
for (const batch of queueIn(locator)) {
|
|
3396
|
+
const partner = partnerOf(batch, locator);
|
|
3397
|
+
if (partner === null || skipped.has(pendingKey(locator, batch.id)))
|
|
3398
|
+
continue;
|
|
3399
|
+
if (queueIn(partner).some((held) => held.id === batch.id))
|
|
3400
|
+
continue;
|
|
3401
|
+
if (unpairedIn(locator, batch))
|
|
3402
|
+
return { locator, batch };
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
return null;
|
|
3406
|
+
}
|
|
3407
|
+
async function unpairing(local, locator, batches) {
|
|
3408
|
+
const ids = new Set(batches.map(({ id }) => id));
|
|
3409
|
+
const leaving = batches.map((batch) => ({
|
|
3410
|
+
batch,
|
|
3411
|
+
returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
|
|
3412
|
+
}));
|
|
3413
|
+
const outcome = localSaves ? 'dropped' : 'unpaired';
|
|
3414
|
+
const current = replicas.get(locator);
|
|
3415
|
+
if (current !== undefined) {
|
|
3416
|
+
const settled = await settlement(local, locator, current, current.record, current.queue.filter((one) => !ids.has(one.id)), { leaving });
|
|
3417
|
+
return {
|
|
3418
|
+
...settled,
|
|
3419
|
+
apply(answer) {
|
|
3420
|
+
for (const batch of batches)
|
|
3421
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
3422
|
+
const set = settled.apply(answer);
|
|
3423
|
+
submitLater(locator);
|
|
3424
|
+
return set;
|
|
3425
|
+
},
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
const queue = queueIn(locator).filter((one) => !ids.has(one.id));
|
|
3429
|
+
const written = new Map();
|
|
3430
|
+
for (const { batch, returned } of leaving) {
|
|
3431
|
+
if (returned === null)
|
|
3432
|
+
continue;
|
|
3433
|
+
for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
|
|
3434
|
+
written.set(saved.path, {
|
|
3435
|
+
locator,
|
|
3436
|
+
path: saved.path,
|
|
3437
|
+
revision: roomDraftRevision(),
|
|
3438
|
+
parts: returnedInto((written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path))?.parts, saved.parts),
|
|
3439
|
+
returned,
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
const carried = carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
|
|
3444
|
+
return {
|
|
3445
|
+
locator,
|
|
3446
|
+
left: batches,
|
|
3447
|
+
commit: {
|
|
3448
|
+
rooms: [],
|
|
3449
|
+
writes: [
|
|
3450
|
+
{
|
|
3451
|
+
locator,
|
|
3452
|
+
queue,
|
|
3453
|
+
roomDrafts: [...written.values()],
|
|
3454
|
+
consumedRoomDrafts: carried.taken,
|
|
3455
|
+
},
|
|
3456
|
+
...carried.writes,
|
|
3457
|
+
],
|
|
3458
|
+
},
|
|
3459
|
+
apply() {
|
|
3460
|
+
for (const batch of batches) {
|
|
3461
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
3462
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
3463
|
+
}
|
|
3464
|
+
if (queue.length > 0)
|
|
3465
|
+
queuesWithoutReplica.set(locator, queue);
|
|
3466
|
+
else
|
|
3467
|
+
queuesWithoutReplica.delete(locator);
|
|
3468
|
+
for (const record of written.values())
|
|
3469
|
+
rememberRoomDraft(record);
|
|
3470
|
+
carried.remember();
|
|
3471
|
+
submitLater(locator);
|
|
3472
|
+
return mergeChangeSets([
|
|
3473
|
+
changeSet(locator, {
|
|
3474
|
+
queue: true,
|
|
3475
|
+
drafts: [...written.keys(), ...carried.taken],
|
|
3476
|
+
}),
|
|
3477
|
+
...carried.changes,
|
|
3478
|
+
]);
|
|
3479
|
+
},
|
|
3480
|
+
};
|
|
3481
|
+
}
|
|
3482
|
+
async function unpair(local) {
|
|
3483
|
+
const handled = new Set();
|
|
3484
|
+
for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
|
|
3485
|
+
const { locator, batch } = held;
|
|
3486
|
+
handled.add(pendingKey(locator, batch.id));
|
|
3487
|
+
publishChangeSet(await together(local, [await unpairing(local, locator, [batch])]));
|
|
3488
|
+
}
|
|
3489
|
+
if (handled.size === 0)
|
|
3490
|
+
return;
|
|
3491
|
+
moveTo({ ...status, ...attention() });
|
|
3492
|
+
}
|
|
3493
|
+
async function discardHeld(head, room, local, signal) {
|
|
3494
|
+
const partner = partnerOf(head, room.locator);
|
|
3495
|
+
const other = partner === null ? undefined : discovered.get(partner);
|
|
3496
|
+
await discardPending(other === undefined ? [room] : [room, other], local, signal);
|
|
3497
|
+
}
|
|
2000
3498
|
async function submitRoom(room, local, authorized, progress, signal) {
|
|
2001
3499
|
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
2002
3500
|
const replica = replicas.get(room.locator);
|
|
2003
3501
|
const head = replica?.queue[0];
|
|
2004
3502
|
if (replica === undefined || head === undefined)
|
|
2005
3503
|
return 'done';
|
|
2006
|
-
|
|
3504
|
+
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
3505
|
+
if (state?.accepted !== undefined)
|
|
3506
|
+
return 'done';
|
|
3507
|
+
const partner = partnerOf(head, room.locator);
|
|
3508
|
+
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3509
|
+
submitLater(partner);
|
|
3510
|
+
return 'done';
|
|
3511
|
+
}
|
|
3512
|
+
if (partner !== null && behindIn(partner, head)) {
|
|
3513
|
+
progress.complete = false;
|
|
2007
3514
|
return 'done';
|
|
2008
|
-
|
|
3515
|
+
}
|
|
2009
3516
|
const { submission, lost: earlier } = state ?? {
|
|
2010
3517
|
submission: { state: 'waiting' },
|
|
2011
3518
|
lost: false,
|
|
@@ -2021,16 +3528,16 @@ export function composeRuntime(options) {
|
|
|
2021
3528
|
}
|
|
2022
3529
|
const lost = earlier || submission.state === 'submitted';
|
|
2023
3530
|
if (lost && pastHorizon(head.id)) {
|
|
2024
|
-
await
|
|
3531
|
+
await discardHeld(head, room, local, signal);
|
|
2025
3532
|
return 'done';
|
|
2026
3533
|
}
|
|
2027
3534
|
let result;
|
|
2028
3535
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
2029
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3536
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
|
|
2030
3537
|
if (discoveryPaused || fences.has(room.locator))
|
|
2031
3538
|
return 'done';
|
|
2032
3539
|
if (signal.aborted || writesHeld()) {
|
|
2033
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3540
|
+
await exclusive(() => mark(local, room.locator, head, submission, earlier));
|
|
2034
3541
|
if (signal.aborted)
|
|
2035
3542
|
return 'done';
|
|
2036
3543
|
progress.complete = false;
|
|
@@ -2038,11 +3545,17 @@ export function composeRuntime(options) {
|
|
|
2038
3545
|
return 'done';
|
|
2039
3546
|
}
|
|
2040
3547
|
const opensBefore = openSignals;
|
|
2041
|
-
const
|
|
3548
|
+
const answeredHere = awaitingAnswer(room.locator);
|
|
3549
|
+
const answeredThere = partner === null ? () => { } : awaitingAnswer(partner);
|
|
3550
|
+
const answered = () => {
|
|
3551
|
+
answeredHere();
|
|
3552
|
+
answeredThere();
|
|
3553
|
+
};
|
|
2042
3554
|
try {
|
|
2043
3555
|
result = await options.adapter.mutate(authorized, {
|
|
2044
3556
|
locator: room.locator,
|
|
2045
3557
|
changeBatch: head,
|
|
3558
|
+
file: batchFile(head),
|
|
2046
3559
|
...(state?.upload === undefined ? {} : { upload: state.upload }),
|
|
2047
3560
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
2048
3561
|
}
|
|
@@ -2067,9 +3580,10 @@ export function composeRuntime(options) {
|
|
|
2067
3580
|
return 'again';
|
|
2068
3581
|
}
|
|
2069
3582
|
if (result.kind === 'room-not-readable') {
|
|
3583
|
+
const unread = discovered.get(result.locator) ?? room;
|
|
2070
3584
|
progress.complete = false;
|
|
2071
|
-
unreadable.add(
|
|
2072
|
-
list(
|
|
3585
|
+
unreadable.add(unread.locator);
|
|
3586
|
+
list(unread, false);
|
|
2073
3587
|
emitRooms();
|
|
2074
3588
|
return 'done';
|
|
2075
3589
|
}
|
|
@@ -2084,24 +3598,30 @@ export function composeRuntime(options) {
|
|
|
2084
3598
|
}
|
|
2085
3599
|
if (result.kind === 'write-halted') {
|
|
2086
3600
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
2087
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3601
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
|
|
2088
3602
|
if (serviceMode === 'open' && openSignals !== opensBefore)
|
|
2089
3603
|
continue;
|
|
2090
3604
|
haltedRefusal = true;
|
|
2091
3605
|
progress.complete = false;
|
|
2092
3606
|
return 'done';
|
|
2093
3607
|
}
|
|
3608
|
+
const other = partner === null ? undefined : discovered.get(partner);
|
|
2094
3609
|
if (result.kind === 'conflicted' ||
|
|
2095
3610
|
result.kind === 'rejected' ||
|
|
2096
3611
|
result.kind === 'expired' ||
|
|
2097
3612
|
result.kind === 'retryable-refusal') {
|
|
2098
3613
|
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2099
3614
|
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2100
|
-
await
|
|
3615
|
+
await discardHeld(head, room, local, signal);
|
|
2101
3616
|
return 'done';
|
|
2102
3617
|
}
|
|
2103
3618
|
if (result.kind === 'expired') {
|
|
2104
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3619
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
3620
|
+
const there = other === undefined ? null : await advance(other, local, authorized, continuing);
|
|
3621
|
+
if (other !== undefined && there !== null) {
|
|
3622
|
+
await withhold(there, other, local, progress);
|
|
3623
|
+
return 'done';
|
|
3624
|
+
}
|
|
2105
3625
|
const withheld = await advance(room, local, authorized, continuing);
|
|
2106
3626
|
if (withheld !== null) {
|
|
2107
3627
|
await withhold(withheld, room, local, progress);
|
|
@@ -2118,11 +3638,25 @@ export function composeRuntime(options) {
|
|
|
2118
3638
|
if (result.kind === 'retryable-refusal') {
|
|
2119
3639
|
const reset = result.reset;
|
|
2120
3640
|
progress.complete = false;
|
|
2121
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3641
|
+
await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
2122
3642
|
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2123
3643
|
}
|
|
2124
3644
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
2125
3645
|
const left = leaving(head, result);
|
|
3646
|
+
const refreshed = async () => ({
|
|
3647
|
+
there: other === undefined ? null : await advance(other, local, authorized, continuing),
|
|
3648
|
+
here: await advance(room, local, authorized, continuing),
|
|
3649
|
+
});
|
|
3650
|
+
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
3651
|
+
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
3652
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
|
|
3653
|
+
if (other !== undefined && judged.there !== null) {
|
|
3654
|
+
await withhold(judged.there, other, local, progress);
|
|
3655
|
+
}
|
|
3656
|
+
if (judged.here !== null)
|
|
3657
|
+
await withhold(judged.here, room, local, progress);
|
|
3658
|
+
return 'done';
|
|
3659
|
+
}
|
|
2126
3660
|
await exclusive(async () => {
|
|
2127
3661
|
if (!continuing())
|
|
2128
3662
|
return;
|
|
@@ -2130,25 +3664,29 @@ export function composeRuntime(options) {
|
|
|
2130
3664
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
2131
3665
|
return;
|
|
2132
3666
|
}
|
|
2133
|
-
const
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
});
|
|
2139
|
-
|
|
3667
|
+
const confirmedLayer = await sliceAt(current.record, 'confirmed', [head]);
|
|
3668
|
+
const held = (result.kind === 'expired' &&
|
|
3669
|
+
(await landedWhole(room.locator, head, confirmedLayer))) ||
|
|
3670
|
+
(result.kind === 'conflicted' &&
|
|
3671
|
+
(await landedWhole(room.locator, head, confirmedLayer, result.affected)));
|
|
3672
|
+
publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
|
|
3673
|
+
await unpair(local);
|
|
2140
3674
|
moveTo({ ...status, ...attention() });
|
|
2141
3675
|
});
|
|
2142
3676
|
if (result.kind === 'expired')
|
|
2143
3677
|
continue;
|
|
2144
|
-
const
|
|
2145
|
-
if (
|
|
2146
|
-
await withhold(
|
|
3678
|
+
const { there, here } = judged ?? (await refreshed());
|
|
3679
|
+
if (other !== undefined && there !== null)
|
|
3680
|
+
await withhold(there, other, local, progress);
|
|
3681
|
+
if (here !== null) {
|
|
3682
|
+
await withhold(here, room, local, progress);
|
|
2147
3683
|
return 'done';
|
|
2148
3684
|
}
|
|
2149
3685
|
continue;
|
|
2150
3686
|
}
|
|
2151
|
-
const kept = await
|
|
3687
|
+
const kept = await (partner === null
|
|
3688
|
+
? adopted(room, local, head, result)
|
|
3689
|
+
: adoptedMove(room, partner, local, head, result)).finally(answered);
|
|
2152
3690
|
if (!kept) {
|
|
2153
3691
|
progress.complete = false;
|
|
2154
3692
|
return 'done';
|
|
@@ -2159,44 +3697,67 @@ export function composeRuntime(options) {
|
|
|
2159
3697
|
function pastHorizon(identity) {
|
|
2160
3698
|
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2161
3699
|
}
|
|
2162
|
-
async function discardPending(
|
|
2163
|
-
const { locator } = room;
|
|
3700
|
+
async function discardPending(rooms, local, signal) {
|
|
2164
3701
|
const discarded = await exclusive(async () => {
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
3702
|
+
const held = rooms
|
|
3703
|
+
.filter(({ locator }) => !fences.has(locator))
|
|
3704
|
+
.map((room) => ({
|
|
3705
|
+
room,
|
|
3706
|
+
before: replicas.get(room.locator),
|
|
3707
|
+
queue: queueIn(room.locator),
|
|
3708
|
+
}));
|
|
3709
|
+
if (held.length === 0)
|
|
3710
|
+
return [];
|
|
3711
|
+
const answer = await local.commit({
|
|
3712
|
+
rooms: held.flatMap(({ room, before }) => before === undefined
|
|
3713
|
+
? []
|
|
3714
|
+
: [
|
|
3715
|
+
{
|
|
3716
|
+
locator: room.locator,
|
|
3717
|
+
key: before.record.key,
|
|
3718
|
+
roots: before.record.typedSelection?.roots ?? [],
|
|
3719
|
+
overlay: { set: [], cleared: [...before.overlaid] },
|
|
3720
|
+
},
|
|
3721
|
+
]),
|
|
3722
|
+
discards: held.map(({ room }) => room.locator),
|
|
3723
|
+
});
|
|
3724
|
+
for (const { room, before, queue } of held) {
|
|
3725
|
+
const { locator } = room;
|
|
3726
|
+
lastFenced.set(locator, ++fencings);
|
|
3727
|
+
for (const batch of queue)
|
|
3728
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
3729
|
+
queuesWithoutReplica.delete(locator);
|
|
3730
|
+
if (before !== undefined) {
|
|
3731
|
+
replicas.set(locator, {
|
|
3732
|
+
...before,
|
|
3733
|
+
queue: [],
|
|
3734
|
+
overlaid: new Set(),
|
|
3735
|
+
count: answer.counts[locator] ?? before.count,
|
|
3736
|
+
});
|
|
3737
|
+
}
|
|
3738
|
+
journal.record(`room discarded ${locator}`);
|
|
2176
3739
|
}
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
if (after !== null)
|
|
2182
|
-
replicas.set(locator, after);
|
|
2183
|
-
journal.record(`room discarded ${locator}`);
|
|
2184
|
-
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
3740
|
+
publishChangeSet(mergeChangeSets(held.map(({ room }) => changeSet(room.locator, {
|
|
3741
|
+
documents: answer.changed[room.locator] ?? [],
|
|
3742
|
+
queue: true,
|
|
3743
|
+
}))));
|
|
2185
3744
|
moveTo({ ...status, ...attention() });
|
|
2186
|
-
return
|
|
2187
|
-
});
|
|
2188
|
-
if (!discarded)
|
|
2189
|
-
return;
|
|
2190
|
-
const progress = { complete: true, reachable: true };
|
|
2191
|
-
await roomTask(locator, async (live) => {
|
|
2192
|
-
await Promise.race([
|
|
2193
|
-
reload(room, local, progress, () => live() && !signal.aborted),
|
|
2194
|
-
abandonedOn(signal),
|
|
2195
|
-
]);
|
|
3745
|
+
return held.map(({ room }) => room);
|
|
2196
3746
|
});
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
3747
|
+
for (const room of discarded) {
|
|
3748
|
+
if (signal.aborted)
|
|
3749
|
+
return;
|
|
3750
|
+
const progress = { complete: true, reachable: true };
|
|
3751
|
+
await roomTask(room.locator, async (live) => {
|
|
3752
|
+
await Promise.race([
|
|
3753
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
3754
|
+
abandonedOn(signal),
|
|
3755
|
+
]);
|
|
3756
|
+
});
|
|
3757
|
+
if (signal.aborted)
|
|
3758
|
+
return;
|
|
3759
|
+
await rest(progress);
|
|
3760
|
+
}
|
|
2200
3761
|
}
|
|
2201
3762
|
function awaitingAnswer(locator) {
|
|
2202
3763
|
let release = () => { };
|
|
@@ -2213,7 +3774,7 @@ export function composeRuntime(options) {
|
|
|
2213
3774
|
async function retainModel(local, room, attached, next) {
|
|
2214
3775
|
const key = modelKey(room.locator, attached);
|
|
2215
3776
|
const previous = modelOf(room.locator, attached);
|
|
2216
|
-
const heldBefore = replicas.get(room.locator)?.
|
|
3777
|
+
const heldBefore = replicas.get(room.locator)?.record;
|
|
2217
3778
|
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
2218
3779
|
(heldBefore === undefined ||
|
|
2219
3780
|
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
@@ -2229,36 +3790,56 @@ export function composeRuntime(options) {
|
|
|
2229
3790
|
if (!continuing())
|
|
2230
3791
|
return;
|
|
2231
3792
|
const held = replicas.get(room.locator);
|
|
3793
|
+
let changed = null;
|
|
2232
3794
|
if (held !== undefined) {
|
|
2233
3795
|
const declared = {
|
|
2234
|
-
...held.
|
|
3796
|
+
...held.record,
|
|
2235
3797
|
operations: room.operations,
|
|
2236
3798
|
replica: room.replica,
|
|
2237
3799
|
typedSelection: room.typedSelection ?? null,
|
|
2238
3800
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
2239
3801
|
};
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
3802
|
+
let read = 'staged';
|
|
3803
|
+
if (!sameMembership(declared, held.record)) {
|
|
3804
|
+
const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
|
|
3805
|
+
read =
|
|
3806
|
+
verdict.verdict !== 'refuse'
|
|
3807
|
+
? 'staged'
|
|
3808
|
+
: verdict.rule === 'model-unavailable'
|
|
3809
|
+
? 'deferred'
|
|
3810
|
+
: { kind: 'refused', refusal: verdict };
|
|
3811
|
+
}
|
|
3812
|
+
const record = read === 'staged'
|
|
2244
3813
|
? declared
|
|
2245
|
-
: {
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
const
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
3814
|
+
: { ...declared, typedSelection: held.record.typedSelection };
|
|
3815
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
|
|
3816
|
+
const replayedLayer = await replayed(record, confirmedLayer, held.queue);
|
|
3817
|
+
const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
|
|
3818
|
+
const answer = await local.commit({
|
|
3819
|
+
rooms: [
|
|
3820
|
+
{
|
|
3821
|
+
locator: room.locator,
|
|
3822
|
+
key: record.key,
|
|
3823
|
+
record: storedRecord(record),
|
|
3824
|
+
roots: record.typedSelection?.roots ?? [],
|
|
3825
|
+
composition: compositionOf(record),
|
|
3826
|
+
overlay: diff.overlay,
|
|
3827
|
+
},
|
|
3828
|
+
],
|
|
3829
|
+
});
|
|
3830
|
+
const { count, indexed } = committedRoom(room.locator, answer);
|
|
3831
|
+
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
|
|
3832
|
+
changed = changeSet(room.locator, {
|
|
3833
|
+
documents: answer.changed[room.locator] ?? [],
|
|
3834
|
+
room: true,
|
|
3835
|
+
});
|
|
3836
|
+
if (read === 'staged' || read === 'deferred') {
|
|
2255
3837
|
if (declarationRefusals.delete(room.locator)) {
|
|
2256
3838
|
sourceRefusals.delete(room.locator);
|
|
2257
3839
|
unreadable.delete(room.locator);
|
|
2258
3840
|
}
|
|
2259
3841
|
}
|
|
2260
|
-
else
|
|
2261
|
-
replicas.set(room.locator, { ...held, confirmed: { ...held.confirmed, record } });
|
|
3842
|
+
else {
|
|
2262
3843
|
refusedSource(room, read);
|
|
2263
3844
|
declarationRefusals.add(room.locator);
|
|
2264
3845
|
}
|
|
@@ -2269,13 +3850,18 @@ export function composeRuntime(options) {
|
|
|
2269
3850
|
invalidModels.delete(key);
|
|
2270
3851
|
}
|
|
2271
3852
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
2272
|
-
if (
|
|
3853
|
+
if (changed === null)
|
|
2273
3854
|
emitRooms();
|
|
2274
3855
|
else
|
|
2275
|
-
publishChangeSet(
|
|
3856
|
+
publishChangeSet(changed);
|
|
2276
3857
|
});
|
|
2277
3858
|
}
|
|
2278
3859
|
async function scoped(result, room, local) {
|
|
3860
|
+
if (result.scope === 'content' && !result.retryable) {
|
|
3861
|
+
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
3862
|
+
await loseRooms([room.locator]);
|
|
3863
|
+
return;
|
|
3864
|
+
}
|
|
2279
3865
|
scopedRefusals.set(room.locator, {
|
|
2280
3866
|
code: result.code,
|
|
2281
3867
|
fields: result.fields,
|
|
@@ -2289,15 +3875,7 @@ export function composeRuntime(options) {
|
|
|
2289
3875
|
});
|
|
2290
3876
|
else if (result.scope !== 'operation') {
|
|
2291
3877
|
unreadable.add(room.locator);
|
|
2292
|
-
scopedAway = {
|
|
2293
|
-
...room,
|
|
2294
|
-
replica: { ...room.replica, admission: 'unavailable' },
|
|
2295
|
-
operations: {
|
|
2296
|
-
...room.operations,
|
|
2297
|
-
...(result.scope === 'content' ? { read: [] } : {}),
|
|
2298
|
-
...(result.scope === 'sync' ? { sync: [] } : {}),
|
|
2299
|
-
},
|
|
2300
|
-
};
|
|
3878
|
+
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
2301
3879
|
await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
|
|
2302
3880
|
}
|
|
2303
3881
|
if (!continuing())
|
|
@@ -2308,30 +3886,22 @@ export function composeRuntime(options) {
|
|
|
2308
3886
|
}
|
|
2309
3887
|
function heldRooms(listed) {
|
|
2310
3888
|
return listed.flatMap((room) => {
|
|
2311
|
-
const record = replicas.get(room.locator)?.
|
|
3889
|
+
const record = replicas.get(room.locator)?.record;
|
|
2312
3890
|
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
2313
3891
|
return [];
|
|
2314
|
-
return [
|
|
2315
|
-
{
|
|
2316
|
-
locator: room.locator,
|
|
2317
|
-
roomVersion: record.roomVersion,
|
|
2318
|
-
documents: record.documents,
|
|
2319
|
-
revisions: record.revisions,
|
|
2320
|
-
},
|
|
2321
|
-
];
|
|
3892
|
+
return [{ locator: room.locator, roomVersion: record.roomVersion }];
|
|
2322
3893
|
});
|
|
2323
3894
|
}
|
|
2324
3895
|
async function synchronize(listed, local, progress, attempt, live) {
|
|
2325
|
-
const
|
|
2326
|
-
const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
|
|
3896
|
+
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
2327
3897
|
if (!live())
|
|
2328
3898
|
return;
|
|
2329
|
-
const ready =
|
|
3899
|
+
const ready = listed.filter((_, index) => {
|
|
2330
3900
|
const outcome = prepared[index];
|
|
2331
3901
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
2332
3902
|
});
|
|
2333
3903
|
if (attempt === 'opened') {
|
|
2334
|
-
options.adapter.roomStream?.hold(heldRooms(ready)
|
|
3904
|
+
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
2335
3905
|
handed = true;
|
|
2336
3906
|
}
|
|
2337
3907
|
discovering?.release();
|
|
@@ -2347,7 +3917,7 @@ export function composeRuntime(options) {
|
|
|
2347
3917
|
progress.complete = false;
|
|
2348
3918
|
return false;
|
|
2349
3919
|
}
|
|
2350
|
-
const prior = replicas.get(room.locator)?.
|
|
3920
|
+
const prior = replicas.get(room.locator)?.record;
|
|
2351
3921
|
if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
|
|
2352
3922
|
list(prior, false);
|
|
2353
3923
|
emitRooms();
|
|
@@ -2359,19 +3929,10 @@ export function composeRuntime(options) {
|
|
|
2359
3929
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
2360
3930
|
if (discoveryPaused || authorization === undefined)
|
|
2361
3931
|
return false;
|
|
2362
|
-
|
|
2363
|
-
if (replicas.has(room.locator))
|
|
2364
|
-
progress.complete = false;
|
|
2365
|
-
return false;
|
|
2366
|
-
}
|
|
2367
|
-
return true;
|
|
3932
|
+
return room.replica.admission === 'admitted';
|
|
2368
3933
|
}
|
|
2369
3934
|
async function caughtUp(room, local, progress, live) {
|
|
2370
|
-
|
|
2371
|
-
if (held === undefined || authorization === undefined || !live())
|
|
2372
|
-
return;
|
|
2373
|
-
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
2374
|
-
if (!behind)
|
|
3935
|
+
if (authorization === undefined || !live() || !behind(room))
|
|
2375
3936
|
return;
|
|
2376
3937
|
const withheld = await advance(room, local, authorization, live);
|
|
2377
3938
|
if (withheld !== null)
|
|
@@ -2399,23 +3960,24 @@ export function composeRuntime(options) {
|
|
|
2399
3960
|
}
|
|
2400
3961
|
async function reload(room, local, progress, live) {
|
|
2401
3962
|
try {
|
|
3963
|
+
if (live())
|
|
3964
|
+
reloadsDue.delete(room.locator);
|
|
2402
3965
|
if (!live() || authorization === undefined)
|
|
2403
3966
|
return;
|
|
2404
3967
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2405
|
-
if (!live())
|
|
2406
|
-
return;
|
|
2407
3968
|
if (bootstrapped.kind !== 'staged') {
|
|
2408
|
-
|
|
3969
|
+
if (live())
|
|
3970
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2409
3971
|
return;
|
|
2410
3972
|
}
|
|
2411
|
-
await publish(local, room, bootstrapped.
|
|
3973
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
2412
3974
|
await caughtUp(room, local, progress, live);
|
|
2413
3975
|
}
|
|
2414
3976
|
finally {
|
|
2415
3977
|
caughtUpTo(room.locator);
|
|
2416
3978
|
}
|
|
2417
3979
|
}
|
|
2418
|
-
async function discoverRooms(local, progress, attempt, live) {
|
|
3980
|
+
async function discoverRooms(local, progress, attempt, live, scope) {
|
|
2419
3981
|
if (closed || authorization === undefined) {
|
|
2420
3982
|
notifyReady();
|
|
2421
3983
|
return;
|
|
@@ -2425,7 +3987,7 @@ export function composeRuntime(options) {
|
|
|
2425
3987
|
const started = discoveries;
|
|
2426
3988
|
let result;
|
|
2427
3989
|
try {
|
|
2428
|
-
result = await options.adapter.discover(authorization);
|
|
3990
|
+
result = await options.adapter.discover(authorization, scope);
|
|
2429
3991
|
}
|
|
2430
3992
|
catch {
|
|
2431
3993
|
result = { kind: 'transport-uncertainty' };
|
|
@@ -2465,12 +4027,13 @@ export function composeRuntime(options) {
|
|
|
2465
4027
|
}
|
|
2466
4028
|
for (const room of result.rooms) {
|
|
2467
4029
|
if (room.operations === undefined ||
|
|
2468
|
-
!['
|
|
4030
|
+
!['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
2469
4031
|
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
2470
4032
|
room.replica === undefined ||
|
|
2471
4033
|
typeof room.replica.selection !== 'string' ||
|
|
2472
4034
|
room.replica.selection.length === 0 ||
|
|
2473
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
4035
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
4036
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
2474
4037
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
2475
4038
|
await refuse('missing-required-guarantee');
|
|
2476
4039
|
notifyReady();
|
|
@@ -2494,15 +4057,16 @@ export function composeRuntime(options) {
|
|
|
2494
4057
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
2495
4058
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
2496
4059
|
limits = result.providerLimits;
|
|
2497
|
-
await local.
|
|
4060
|
+
await local.fact('providerLimits', limits);
|
|
4061
|
+
await local.fact('capabilities', capabilities);
|
|
2498
4062
|
}
|
|
2499
4063
|
const signedIn = result.member ?? null;
|
|
2500
4064
|
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
2501
|
-
await local.
|
|
4065
|
+
await local.fact('member', signedIn);
|
|
2502
4066
|
member = signedIn;
|
|
2503
4067
|
emitMember();
|
|
2504
4068
|
}
|
|
2505
|
-
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
4069
|
+
await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
|
|
2506
4070
|
}
|
|
2507
4071
|
async function ensureAuthorization() {
|
|
2508
4072
|
if (authorization !== undefined)
|
|
@@ -2599,9 +4163,44 @@ export function composeRuntime(options) {
|
|
|
2599
4163
|
options.adapter.roomStream?.reopen();
|
|
2600
4164
|
});
|
|
2601
4165
|
}
|
|
2602
|
-
function
|
|
4166
|
+
function rediscover(scope) {
|
|
4167
|
+
return task(OPENING_LANE, async (live) => {
|
|
4168
|
+
if (!live() || !socketOpen)
|
|
4169
|
+
return;
|
|
4170
|
+
const progress = { complete: true, reachable: true };
|
|
4171
|
+
const gate = discover();
|
|
4172
|
+
moveTo(transferring(status));
|
|
4173
|
+
try {
|
|
4174
|
+
const local = await openLocal();
|
|
4175
|
+
if (!live())
|
|
4176
|
+
return;
|
|
4177
|
+
await ensureAuthorization();
|
|
4178
|
+
if (!live())
|
|
4179
|
+
return;
|
|
4180
|
+
await discoverRooms(local, progress, 'authorized', live, scope);
|
|
4181
|
+
}
|
|
4182
|
+
catch {
|
|
4183
|
+
progress.complete = false;
|
|
4184
|
+
}
|
|
4185
|
+
finally {
|
|
4186
|
+
gate.release();
|
|
4187
|
+
notifyReady();
|
|
4188
|
+
}
|
|
4189
|
+
if (!live())
|
|
4190
|
+
return;
|
|
4191
|
+
if (!progress.complete)
|
|
4192
|
+
complete = false;
|
|
4193
|
+
await rest(progress, live);
|
|
4194
|
+
});
|
|
4195
|
+
}
|
|
4196
|
+
function closedSocket(revoked, lost) {
|
|
2603
4197
|
const opened = socketOpen;
|
|
2604
4198
|
socketOpen = false;
|
|
4199
|
+
connectionLost();
|
|
4200
|
+
if (lost) {
|
|
4201
|
+
reads.lost();
|
|
4202
|
+
conversations.lost();
|
|
4203
|
+
}
|
|
2605
4204
|
if (revoked) {
|
|
2606
4205
|
void refuse('revoked-authorization');
|
|
2607
4206
|
return;
|
|
@@ -2618,9 +4217,10 @@ export function composeRuntime(options) {
|
|
|
2618
4217
|
function applyOpening(expired) {
|
|
2619
4218
|
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2620
4219
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
2621
|
-
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2622
4220
|
if (opened === generation)
|
|
2623
4221
|
opened = null;
|
|
4222
|
+
const reloading = progress === null ? [] : owedReloads(expired);
|
|
4223
|
+
const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
|
|
2624
4224
|
return task(OPENING_LANE, async (live) => {
|
|
2625
4225
|
underway = null;
|
|
2626
4226
|
if (!live())
|
|
@@ -2630,24 +4230,17 @@ export function composeRuntime(options) {
|
|
|
2630
4230
|
await rest(cycle, live);
|
|
2631
4231
|
return;
|
|
2632
4232
|
}
|
|
2633
|
-
await roomsCarried(expired);
|
|
4233
|
+
await roomsCarried([...expired, ...reloading]);
|
|
2634
4234
|
if (!live())
|
|
2635
4235
|
return;
|
|
2636
|
-
const replaced = new Set(owed);
|
|
2637
|
-
owed.clear();
|
|
2638
4236
|
try {
|
|
2639
4237
|
const local = await openLocal();
|
|
2640
|
-
const
|
|
2641
|
-
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2642
|
-
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2643
|
-
for (const room of reloading)
|
|
2644
|
-
catching.add(room.locator);
|
|
2645
|
-
readSide.recover();
|
|
2646
|
-
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2647
|
-
const reloaded = [...(await reloads), ...(await replacing)];
|
|
4238
|
+
const reloaded = await replacing;
|
|
2648
4239
|
if (progress.reachable) {
|
|
2649
|
-
for (const room of
|
|
2650
|
-
if (
|
|
4240
|
+
for (const room of discovered.values()) {
|
|
4241
|
+
if (replicas.has(room.locator) &&
|
|
4242
|
+
!expired.includes(room.locator) &&
|
|
4243
|
+
!reloading.includes(room.locator)) {
|
|
2651
4244
|
await confirmModels(room, local);
|
|
2652
4245
|
}
|
|
2653
4246
|
}
|
|
@@ -2664,14 +4257,29 @@ export function composeRuntime(options) {
|
|
|
2664
4257
|
await finishOpening(progress, live);
|
|
2665
4258
|
});
|
|
2666
4259
|
}
|
|
4260
|
+
function owedReloads(expired) {
|
|
4261
|
+
const reloading = [...discovered.values()]
|
|
4262
|
+
.filter((room) => replicas.has(room.locator) &&
|
|
4263
|
+
!expired.includes(room.locator) &&
|
|
4264
|
+
(owed.has(room.locator) || revoked(room.locator)))
|
|
4265
|
+
.map((room) => room.locator);
|
|
4266
|
+
owed.clear();
|
|
4267
|
+
for (const locator of reloading)
|
|
4268
|
+
catching.add(locator);
|
|
4269
|
+
liveQueries.recover();
|
|
4270
|
+
return reloading;
|
|
4271
|
+
}
|
|
2667
4272
|
async function reloadNow(replaced, progress) {
|
|
4273
|
+
const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
|
|
4274
|
+
for (const room of reloaded)
|
|
4275
|
+
reloadsDue.set(room.locator, generation);
|
|
2668
4276
|
try {
|
|
2669
4277
|
const local = await openLocal();
|
|
2670
|
-
return await Promise.allSettled(
|
|
2671
|
-
.filter((room) => replicas.has(room.locator) && replaced(room))
|
|
2672
|
-
.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
4278
|
+
return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
2673
4279
|
}
|
|
2674
4280
|
catch {
|
|
4281
|
+
for (const room of reloaded)
|
|
4282
|
+
reloadsDue.delete(room.locator);
|
|
2675
4283
|
progress.reachable = false;
|
|
2676
4284
|
return [];
|
|
2677
4285
|
}
|
|
@@ -2788,6 +4396,7 @@ export function composeRuntime(options) {
|
|
|
2788
4396
|
progress.complete = false;
|
|
2789
4397
|
return;
|
|
2790
4398
|
}
|
|
4399
|
+
reads.applied();
|
|
2791
4400
|
submitAll();
|
|
2792
4401
|
for (const locator of stale)
|
|
2793
4402
|
if (repairable.has(locator))
|
|
@@ -2796,10 +4405,11 @@ export function composeRuntime(options) {
|
|
|
2796
4405
|
if (status.providerUnreachable)
|
|
2797
4406
|
conversations.reconnected();
|
|
2798
4407
|
const reading = { complete: true, reachable: true };
|
|
2799
|
-
|
|
2800
|
-
await conversations.catchUp(reading);
|
|
4408
|
+
await conversations.catchUp(reading);
|
|
2801
4409
|
if (progress.reachable && reading.reachable)
|
|
2802
4410
|
await conversations.resend(reading);
|
|
4411
|
+
if (progress.reachable)
|
|
4412
|
+
void recovery.attempt('calls', resendCalls);
|
|
2803
4413
|
if (!reading.reachable) {
|
|
2804
4414
|
progress.reachable = false;
|
|
2805
4415
|
resendLater();
|
|
@@ -2823,7 +4433,7 @@ export function composeRuntime(options) {
|
|
|
2823
4433
|
if (synced && !recorded) {
|
|
2824
4434
|
const completedAt = new Date().toISOString();
|
|
2825
4435
|
try {
|
|
2826
|
-
await (await openLocal()).
|
|
4436
|
+
await (await openLocal()).fact('lastCompleted', completedAt);
|
|
2827
4437
|
if (!live())
|
|
2828
4438
|
return;
|
|
2829
4439
|
recorded = true;
|
|
@@ -2860,11 +4470,11 @@ export function composeRuntime(options) {
|
|
|
2860
4470
|
return;
|
|
2861
4471
|
const room = discovered.get(locator);
|
|
2862
4472
|
const held = replicas.get(locator);
|
|
2863
|
-
if (room === undefined || room.replica.admission !== 'admitted'
|
|
4473
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2864
4474
|
return;
|
|
2865
4475
|
if (held !== undefined &&
|
|
2866
|
-
(held.
|
|
2867
|
-
options.adapter.reached(locator, held.
|
|
4476
|
+
(held.record.replica.selection !== room.replica.selection ||
|
|
4477
|
+
options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
|
|
2868
4478
|
return;
|
|
2869
4479
|
}
|
|
2870
4480
|
const during = underway?.generation === generation ? underway.progress : null;
|
|
@@ -2911,18 +4521,29 @@ export function composeRuntime(options) {
|
|
|
2911
4521
|
return;
|
|
2912
4522
|
if (event.kind === 'opened') {
|
|
2913
4523
|
socketOpen = true;
|
|
4524
|
+
connectionLive();
|
|
2914
4525
|
generation += 1;
|
|
4526
|
+
reads.opened();
|
|
2915
4527
|
conversations.reopened();
|
|
2916
4528
|
recovery.resume();
|
|
2917
4529
|
void sync('opened');
|
|
2918
4530
|
}
|
|
2919
4531
|
else if (event.kind === 'closed')
|
|
2920
|
-
closedSocket(event.revoked);
|
|
4532
|
+
closedSocket(event.revoked, event.lost);
|
|
2921
4533
|
else if (event.kind === 'opening')
|
|
2922
4534
|
void applyOpening(event.expired);
|
|
4535
|
+
else if (event.kind === 'authority-changed')
|
|
4536
|
+
void rediscover(event.scope);
|
|
2923
4537
|
else
|
|
2924
4538
|
void applyRoom(event.locator, event.roomVersion);
|
|
2925
|
-
}, receipts);
|
|
4539
|
+
}, receipts, heldRevisions);
|
|
4540
|
+
}
|
|
4541
|
+
async function heldRevisions(locator, paths) {
|
|
4542
|
+
const held = replicas.get(locator);
|
|
4543
|
+
if (held === undefined || paths.length === 0)
|
|
4544
|
+
return new Map();
|
|
4545
|
+
const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
|
|
4546
|
+
return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
|
|
2926
4547
|
}
|
|
2927
4548
|
function writesHeld() {
|
|
2928
4549
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -2974,7 +4595,7 @@ export function composeRuntime(options) {
|
|
|
2974
4595
|
if (!live())
|
|
2975
4596
|
return;
|
|
2976
4597
|
await conversations.reconcile();
|
|
2977
|
-
if (conversations.current()
|
|
4598
|
+
if (conversations.current()) {
|
|
2978
4599
|
show();
|
|
2979
4600
|
return;
|
|
2980
4601
|
}
|
|
@@ -2992,10 +4613,12 @@ export function composeRuntime(options) {
|
|
|
2992
4613
|
if (closed)
|
|
2993
4614
|
return;
|
|
2994
4615
|
closed = true;
|
|
2995
|
-
if (
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
4616
|
+
if (probing !== null)
|
|
4617
|
+
clearInterval(probing);
|
|
4618
|
+
for (const resolve of shownWaiters.splice(0))
|
|
4619
|
+
resolve();
|
|
4620
|
+
connectionLive();
|
|
4621
|
+
lockListeners.clear();
|
|
2999
4622
|
readinessWatches.clear();
|
|
3000
4623
|
withdrawStream?.();
|
|
3001
4624
|
withdrawStream = undefined;
|
|
@@ -3004,17 +4627,13 @@ export function composeRuntime(options) {
|
|
|
3004
4627
|
withdrawServiceMode?.();
|
|
3005
4628
|
conversations.close();
|
|
3006
4629
|
void recovery.retire();
|
|
3007
|
-
store?.close();
|
|
3008
|
-
void opening?.then((opened) => {
|
|
3009
|
-
opened.close();
|
|
3010
|
-
}, () => { });
|
|
3011
4630
|
roomListeners.clear();
|
|
3012
4631
|
refusalListeners.clear();
|
|
3013
4632
|
storageLossListeners.clear();
|
|
3014
4633
|
statusListeners.clear();
|
|
3015
4634
|
serviceModeListeners.clear();
|
|
3016
4635
|
memberListeners.clear();
|
|
3017
|
-
|
|
4636
|
+
liveQueries.close();
|
|
3018
4637
|
queueSubscriptions.clear();
|
|
3019
4638
|
roomDraftListeners.clear();
|
|
3020
4639
|
presentationListeners.clear();
|
|
@@ -3041,56 +4660,98 @@ export function composeRuntime(options) {
|
|
|
3041
4660
|
if (options.adapter.roomStream === undefined)
|
|
3042
4661
|
void sync('unstreamed');
|
|
3043
4662
|
function planned(input, reach) {
|
|
3044
|
-
|
|
4663
|
+
const plan = planOf(input, roomFacts(reach), planReach(reach));
|
|
4664
|
+
const named = plan.paths !== null &&
|
|
4665
|
+
plan.rooms !== null &&
|
|
4666
|
+
plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
|
|
4667
|
+
return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
|
|
3045
4668
|
}
|
|
3046
4669
|
function watchPlan(plan, deliver) {
|
|
3047
|
-
|
|
4670
|
+
let seeded = false;
|
|
4671
|
+
let pending = true;
|
|
4672
|
+
const shown = () => {
|
|
4673
|
+
const seed = seeds.get(plan.key);
|
|
4674
|
+
if (seed === undefined || seeded || !pending)
|
|
4675
|
+
return;
|
|
4676
|
+
seeded = true;
|
|
4677
|
+
deliver(seededResult(seed));
|
|
4678
|
+
};
|
|
4679
|
+
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
4680
|
+
seedWatchers.set(plan.key, watchers);
|
|
4681
|
+
watchers.add(shown);
|
|
4682
|
+
const stop = liveQueries.watch(plan, identityOf(plan), () => {
|
|
3048
4683
|
const cover = covered(plan);
|
|
3049
4684
|
awaitPreparation(cover);
|
|
3050
|
-
return cover;
|
|
3051
|
-
},
|
|
4685
|
+
return seedBehind(plan, cover);
|
|
4686
|
+
}, (message) => {
|
|
4687
|
+
const answered = message;
|
|
4688
|
+
pending = whole(answered) && 'pending' in answered.result;
|
|
4689
|
+
if (pending && seeds.has(plan.key)) {
|
|
4690
|
+
shown();
|
|
4691
|
+
return;
|
|
4692
|
+
}
|
|
4693
|
+
seeded = false;
|
|
4694
|
+
deliver(message);
|
|
4695
|
+
});
|
|
4696
|
+
return () => {
|
|
4697
|
+
watchers.delete(shown);
|
|
4698
|
+
if (watchers.size === 0)
|
|
4699
|
+
seedWatchers.delete(plan.key);
|
|
4700
|
+
stop();
|
|
4701
|
+
};
|
|
3052
4702
|
}
|
|
3053
|
-
async function searched(input, reach) {
|
|
3054
|
-
const
|
|
3055
|
-
const filter =
|
|
4703
|
+
async function searched(input, reach, locked) {
|
|
4704
|
+
const asked = searchPlanOf(input);
|
|
4705
|
+
const filter = {
|
|
4706
|
+
...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
|
|
4707
|
+
locked,
|
|
4708
|
+
};
|
|
4709
|
+
const unlocking = (filter.rooms ?? [])
|
|
4710
|
+
.filter((room) => locked.includes(room))
|
|
4711
|
+
.map((room) => ({ room, awaits: 'unlock' }));
|
|
3056
4712
|
for (;;) {
|
|
3057
4713
|
const cover = covered(filter);
|
|
3058
4714
|
awaitPreparation(cover);
|
|
3059
|
-
const pending = 'pending' in cover ?
|
|
3060
|
-
const
|
|
3061
|
-
const
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
4715
|
+
const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
|
|
4716
|
+
const complete = pending.length === 0 && !('pending' in cover);
|
|
4717
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
4718
|
+
const answering = complete
|
|
4719
|
+
? filter
|
|
4720
|
+
: { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
|
|
4721
|
+
const answered = covered(answering);
|
|
4722
|
+
if ('pending' in answered)
|
|
4723
|
+
continue;
|
|
4724
|
+
const listed = conversations.searchable();
|
|
3067
4725
|
const named = reach !== null && !reach.conversations
|
|
3068
4726
|
? []
|
|
3069
|
-
: (
|
|
3070
|
-
const
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
continue;
|
|
3085
|
-
return {
|
|
3086
|
-
...searchIndex.search(plan, selected === null ? null : selected.documents, named),
|
|
3087
|
-
coverage: {
|
|
3088
|
-
rooms: covering,
|
|
3089
|
-
conversations: named,
|
|
3090
|
-
index: searchIndex.generation(),
|
|
3091
|
-
overlay: cover.coverage.overlay,
|
|
4727
|
+
: (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
4728
|
+
const hits = await (await openLocal()).search({
|
|
4729
|
+
text: asked.text,
|
|
4730
|
+
limit: capped(asked.limit),
|
|
4731
|
+
after: asked.after,
|
|
4732
|
+
total: asked.total,
|
|
4733
|
+
documents: {
|
|
4734
|
+
plan: answering,
|
|
4735
|
+
rooms: answered.rooms,
|
|
4736
|
+
beneath: asked.collection
|
|
4737
|
+
? answered.rooms.map(({ locator, key }) => ({
|
|
4738
|
+
key,
|
|
4739
|
+
roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
|
|
4740
|
+
}))
|
|
4741
|
+
: [],
|
|
3092
4742
|
},
|
|
4743
|
+
conversations: named,
|
|
4744
|
+
});
|
|
4745
|
+
if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
4746
|
+
continue;
|
|
4747
|
+
const coverage = {
|
|
4748
|
+
rooms: answered.coverage.rooms,
|
|
4749
|
+
conversations: named,
|
|
4750
|
+
overlay: answered.coverage.overlay,
|
|
3093
4751
|
};
|
|
4752
|
+
return complete
|
|
4753
|
+
? { ...hits, coverage }
|
|
4754
|
+
: { pending, documents: hits.documents, messages: hits.messages, coverage };
|
|
3094
4755
|
}
|
|
3095
4756
|
}
|
|
3096
4757
|
const watch = (topic, input, deliver) => {
|
|
@@ -3099,13 +4760,17 @@ export function composeRuntime(options) {
|
|
|
3099
4760
|
roomDraftListeners.add(listener);
|
|
3100
4761
|
void exclusive(async () => {
|
|
3101
4762
|
await openLocal();
|
|
3102
|
-
if (closed || !roomDraftListeners.has(listener))
|
|
3103
|
-
return;
|
|
3104
|
-
listener.ready = true;
|
|
3105
4763
|
}).then(() => {
|
|
3106
|
-
if (closed || !
|
|
4764
|
+
if (closed || !roomDraftListeners.has(listener))
|
|
3107
4765
|
return;
|
|
3108
|
-
|
|
4766
|
+
const snapshot = snapshotRoomDrafts();
|
|
4767
|
+
draftsShown = draftsShown.then(async () => {
|
|
4768
|
+
const value = await snapshot.catch(() => null);
|
|
4769
|
+
if (value === null || closed || !roomDraftListeners.has(listener))
|
|
4770
|
+
return;
|
|
4771
|
+
listener.ready = true;
|
|
4772
|
+
listener.notify(value);
|
|
4773
|
+
});
|
|
3109
4774
|
}, () => { });
|
|
3110
4775
|
return () => {
|
|
3111
4776
|
roomDraftListeners.delete(listener);
|
|
@@ -3138,7 +4803,7 @@ export function composeRuntime(options) {
|
|
|
3138
4803
|
};
|
|
3139
4804
|
}
|
|
3140
4805
|
if (topic === 'readiness') {
|
|
3141
|
-
return watchReadiness({ target: '', code: 'stored',
|
|
4806
|
+
return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
|
|
3142
4807
|
}
|
|
3143
4808
|
if (topic === 'rooms') {
|
|
3144
4809
|
roomListeners.add(deliver);
|
|
@@ -3200,7 +4865,7 @@ export function composeRuntime(options) {
|
|
|
3200
4865
|
return listed();
|
|
3201
4866
|
},
|
|
3202
4867
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
3203
|
-
search: (input) => searched(input, null),
|
|
4868
|
+
search: (input) => searched(input, null, []),
|
|
3204
4869
|
types(locator) {
|
|
3205
4870
|
return {
|
|
3206
4871
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -3220,46 +4885,32 @@ export function composeRuntime(options) {
|
|
|
3220
4885
|
return listed;
|
|
3221
4886
|
});
|
|
3222
4887
|
},
|
|
3223
|
-
read(room, path) {
|
|
4888
|
+
async read(room, path) {
|
|
3224
4889
|
const record = roomDrafts.get(room)?.get(path);
|
|
3225
|
-
return record === undefined ? null :
|
|
4890
|
+
return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
|
|
3226
4891
|
},
|
|
3227
|
-
write(room, path, revision, input) {
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
const
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
? undefined
|
|
3239
|
-
: await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
|
|
3240
|
-
const record = {
|
|
3241
|
-
locator: room,
|
|
3242
|
-
path,
|
|
3243
|
-
revision: roomDraftRevision(),
|
|
3244
|
-
parts: held(current?.parts, parts, file),
|
|
3245
|
-
returned: current?.returned ?? null,
|
|
3246
|
-
};
|
|
3247
|
-
await local.keepRoomDraft(record);
|
|
3248
|
-
rememberRoomDraft(record);
|
|
3249
|
-
publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
|
|
3250
|
-
return { outcome: 'written', roomDraft: roomDraftOf(record) };
|
|
3251
|
-
});
|
|
4892
|
+
write(room, path, revision, input, next = roomDraftRevision()) {
|
|
4893
|
+
if (typeof next !== 'string' || next === '')
|
|
4894
|
+
return Promise.reject(runtimeError('room-draft-malformed'));
|
|
4895
|
+
return gathered(`room-draft\u0000${room}\u0000${path}`, { revision, input, next }, (earlier, later) => {
|
|
4896
|
+
if (later.revision !== earlier.next)
|
|
4897
|
+
return null;
|
|
4898
|
+
const parts = gatheredParts(earlier.input, later.input);
|
|
4899
|
+
return parts === null
|
|
4900
|
+
? null
|
|
4901
|
+
: { revision: earlier.revision, input: parts, next: later.next };
|
|
4902
|
+
}, (written) => writtenRoomDraft(room, path, written.revision, written.input, written.next));
|
|
3252
4903
|
},
|
|
3253
4904
|
discard(room, path, revision) {
|
|
3254
4905
|
return exclusive(async () => {
|
|
3255
4906
|
const local = await openLocal();
|
|
3256
4907
|
const current = roomDrafts.get(room)?.get(path);
|
|
3257
4908
|
if (current === undefined || current.revision !== revision) {
|
|
3258
|
-
return staleWrite(current === undefined ? null :
|
|
4909
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
3259
4910
|
}
|
|
3260
4911
|
await local.discardRoomDraft(room, path);
|
|
3261
4912
|
forgetRoomDraft(room, path);
|
|
3262
|
-
publishChangeSet(changeSet(room,
|
|
4913
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
3263
4914
|
moveTo({ ...status, ...attention() });
|
|
3264
4915
|
return { outcome: 'written', roomDraft: null };
|
|
3265
4916
|
});
|
|
@@ -3275,29 +4926,38 @@ export function composeRuntime(options) {
|
|
|
3275
4926
|
});
|
|
3276
4927
|
},
|
|
3277
4928
|
write(app, value) {
|
|
3278
|
-
return exclusive(
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
4929
|
+
return exclusive(() => keptPresentation(app, () => structuredClone(value)));
|
|
4930
|
+
},
|
|
4931
|
+
edit(app, members) {
|
|
4932
|
+
return gathered(`presentation\u0000${app}`, structuredClone(members), (earlier, later) => ({ ...earlier, ...later }), (edited) => keptPresentation(app, (held) => Object.fromEntries([
|
|
4933
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(edited, member)),
|
|
4934
|
+
...Object.entries(edited).filter(([, value]) => value !== null),
|
|
4935
|
+
])));
|
|
3285
4936
|
},
|
|
3286
4937
|
},
|
|
3287
4938
|
save(room, named, writtenBy) {
|
|
3288
4939
|
return exclusive(() => saveRoomDrafts(room, named, writtenBy));
|
|
3289
4940
|
},
|
|
4941
|
+
saveParts(room, parts, writtenBy) {
|
|
4942
|
+
return exclusive(() => saveParts(room, parts, writtenBy));
|
|
4943
|
+
},
|
|
3290
4944
|
history(locator, path, query) {
|
|
3291
4945
|
return readHistory(locator, path, query);
|
|
3292
4946
|
},
|
|
4947
|
+
resolve(room, path, destination) {
|
|
4948
|
+
return resolveReference(room, path, destination);
|
|
4949
|
+
},
|
|
3293
4950
|
binaryFile: {
|
|
4951
|
+
part: readPart,
|
|
3294
4952
|
read: readBinaryPath,
|
|
3295
|
-
|
|
4953
|
+
link: linkBinaryPath,
|
|
3296
4954
|
},
|
|
3297
|
-
call(name, input) {
|
|
3298
|
-
|
|
4955
|
+
async call(name, input) {
|
|
4956
|
+
await ready;
|
|
4957
|
+
const room = calledRooms(name, input).find((one) => locked(one));
|
|
4958
|
+
return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
|
|
3299
4959
|
},
|
|
3300
|
-
reached(locator, roomVersion, path) {
|
|
4960
|
+
async reached(locator, roomVersion, path) {
|
|
3301
4961
|
return path === undefined
|
|
3302
4962
|
? reachedBy(locator, roomVersion)
|
|
3303
4963
|
: writtenAfter(locator, roomVersion, path);
|
|
@@ -3305,14 +4965,16 @@ export function composeRuntime(options) {
|
|
|
3305
4965
|
log() {
|
|
3306
4966
|
return journal.export();
|
|
3307
4967
|
},
|
|
4968
|
+
cleanUp,
|
|
3308
4969
|
persistence() {
|
|
3309
4970
|
return persistence === 'granted';
|
|
3310
4971
|
},
|
|
3311
4972
|
async retry() {
|
|
3312
4973
|
journal.record('retry');
|
|
3313
|
-
|
|
4974
|
+
unpreparable.clear();
|
|
3314
4975
|
queueReadiness();
|
|
3315
|
-
|
|
4976
|
+
if (full)
|
|
4977
|
+
await probed();
|
|
3316
4978
|
},
|
|
3317
4979
|
observe: observing(watch),
|
|
3318
4980
|
async reauthorize() {
|
|
@@ -3334,20 +4996,22 @@ export function composeRuntime(options) {
|
|
|
3334
4996
|
const reachable = {
|
|
3335
4997
|
md,
|
|
3336
4998
|
watch,
|
|
3337
|
-
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
3338
4999
|
plan: planned,
|
|
3339
5000
|
query: (plan) => rowsQuery(plan),
|
|
3340
5001
|
watchPlan,
|
|
3341
5002
|
search: searched,
|
|
3342
|
-
searching,
|
|
3343
5003
|
watchReadiness,
|
|
5004
|
+
locked,
|
|
5005
|
+
lockedRooms,
|
|
5006
|
+
watchLocks,
|
|
5007
|
+
located,
|
|
3344
5008
|
};
|
|
3345
5009
|
return {
|
|
3346
5010
|
md,
|
|
5011
|
+
seed,
|
|
3347
5012
|
stop,
|
|
3348
5013
|
lose: loseRooms,
|
|
3349
5014
|
watch,
|
|
3350
|
-
acquire,
|
|
3351
5015
|
persisted(answer) {
|
|
3352
5016
|
if (answer === persistence)
|
|
3353
5017
|
return;
|
|
@@ -3355,6 +5019,11 @@ export function composeRuntime(options) {
|
|
|
3355
5019
|
journal.record(`persistence ${answer}`);
|
|
3356
5020
|
queueReadiness();
|
|
3357
5021
|
},
|
|
3358
|
-
|
|
5022
|
+
shown: screen,
|
|
5023
|
+
within: (reach, port) => {
|
|
5024
|
+
if (full)
|
|
5025
|
+
void probed();
|
|
5026
|
+
return narrowed(reachable, reach, port);
|
|
5027
|
+
},
|
|
3359
5028
|
};
|
|
3360
5029
|
}
|