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