@agentshouse/mdruntime 0.1.0-c3903a68dcbbf → 0.1.0-c3fa75348e547
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +270 -181
- package/README.md +22 -12
- package/dist/acquisition.d.ts +14 -0
- package/dist/acquisition.d.ts.map +1 -0
- package/dist/acquisition.js +24 -0
- package/dist/adapter.d.ts +160 -67
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +1 -9
- package/dist/attached-model.d.ts +11 -0
- package/dist/attached-model.d.ts.map +1 -0
- package/dist/attached-model.js +47 -0
- package/dist/attempt.d.ts +16 -0
- package/dist/attempt.d.ts.map +1 -0
- package/dist/attempt.js +70 -0
- package/dist/binary.d.ts +23 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +8 -0
- package/dist/bootstrap.d.ts +21 -6
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +114 -38
- package/dist/change-set.d.ts +2 -0
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +50 -21
- package/dist/change.d.ts +15 -0
- package/dist/change.d.ts.map +1 -1
- package/dist/change.js +26 -1
- package/dist/chat/acts.d.ts +33 -0
- package/dist/chat/acts.d.ts.map +1 -0
- package/dist/chat/acts.js +36 -0
- package/dist/chat/context.d.ts +3 -0
- package/dist/chat/context.d.ts.map +1 -0
- package/dist/chat/context.js +10 -0
- package/dist/chat/input.d.ts +9 -0
- package/dist/chat/input.d.ts.map +1 -0
- package/dist/chat/input.js +99 -0
- package/dist/chat/kit.d.ts +61 -0
- package/dist/chat/kit.d.ts.map +1 -0
- package/dist/chat/kit.js +123 -0
- package/dist/chat/lasting.d.ts +6 -0
- package/dist/chat/lasting.d.ts.map +1 -0
- package/dist/chat/lasting.js +19 -0
- package/dist/chat/list.d.ts +27 -0
- package/dist/chat/list.d.ts.map +1 -0
- package/dist/chat/list.js +63 -0
- package/dist/chat/markdown.d.ts +20 -0
- package/dist/chat/markdown.d.ts.map +1 -0
- package/dist/chat/markdown.js +188 -0
- package/dist/chat/message.d.ts +6 -0
- package/dist/chat/message.d.ts.map +1 -0
- package/dist/chat/message.js +192 -0
- package/dist/chat/messages.d.ts +22 -0
- package/dist/chat/messages.d.ts.map +1 -0
- package/dist/chat/messages.js +155 -0
- package/dist/chat/name.d.ts +6 -0
- package/dist/chat/name.d.ts.map +1 -0
- package/dist/chat/name.js +37 -0
- package/dist/chat/opened.d.ts +11 -0
- package/dist/chat/opened.d.ts.map +1 -0
- package/dist/chat/opened.js +17 -0
- package/dist/chat/overlay.d.ts +31 -0
- package/dist/chat/overlay.d.ts.map +1 -0
- package/dist/chat/overlay.js +97 -0
- package/dist/chat/parts.d.ts +57 -0
- package/dist/chat/parts.d.ts.map +1 -0
- package/dist/chat/parts.js +119 -0
- package/dist/chat/process.d.ts +20 -0
- package/dist/chat/process.d.ts.map +1 -0
- package/dist/chat/process.js +32 -0
- package/dist/chat/provider.d.ts +63 -0
- package/dist/chat/provider.d.ts.map +1 -0
- package/dist/chat/provider.js +629 -0
- package/dist/chat/question.d.ts +9 -0
- package/dist/chat/question.d.ts.map +1 -0
- package/dist/chat/question.js +80 -0
- package/dist/chat/questions.d.ts +31 -0
- package/dist/chat/questions.d.ts.map +1 -0
- package/dist/chat/questions.js +111 -0
- package/dist/chat/read.d.ts +5 -0
- package/dist/chat/read.d.ts.map +1 -0
- package/dist/chat/read.js +13 -0
- package/dist/chat/ready.d.ts +34 -0
- package/dist/chat/ready.d.ts.map +1 -0
- package/dist/chat/ready.js +91 -0
- package/dist/chat/style.d.ts +4 -0
- package/dist/chat/style.d.ts.map +1 -0
- package/dist/chat/style.js +302 -0
- package/dist/chat.d.ts +14 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +8 -0
- package/dist/client.d.ts +28 -20
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +78 -32
- package/dist/collection.d.ts +21 -14
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +270 -32
- package/dist/connection.d.ts +50 -38
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +2245 -876
- package/dist/conversation.d.ts +46 -19
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +632 -221
- package/dist/durability.d.ts +7 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/entry.d.ts +17 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +156 -0
- package/dist/expectation.d.ts +21 -0
- package/dist/expectation.d.ts.map +1 -0
- package/dist/expectation.js +70 -0
- package/dist/failure.d.ts +13 -0
- package/dist/failure.d.ts.map +1 -0
- package/dist/failure.js +19 -0
- package/dist/fence.d.ts +6 -0
- package/dist/fence.d.ts.map +1 -0
- package/dist/fence.js +10 -0
- package/dist/host.d.ts +24 -3
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +128 -70
- package/dist/identity.d.ts +5 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +19 -1
- package/dist/index.d.ts +16 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/memory.d.ts +21 -7
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +164 -47
- package/dist/model.d.ts +17 -14
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +16 -4
- package/dist/observation.d.ts +6 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +38 -17
- package/dist/open.d.ts +2 -0
- package/dist/open.d.ts.map +1 -0
- package/dist/open.js +1 -0
- package/dist/outcome.d.ts +35 -22
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +42 -1
- package/dist/overlay.d.ts +16 -3
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +118 -77
- package/dist/paths.d.ts +0 -1
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +0 -16
- package/dist/query.d.ts +47 -6
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +86 -38
- package/dist/reach.d.ts +45 -0
- package/dist/reach.d.ts.map +1 -0
- package/dist/reach.js +306 -0
- package/dist/react.d.ts +44 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +66 -0
- package/dist/read-side.d.ts +16 -5
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +408 -137
- package/dist/readiness.d.ts +53 -0
- package/dist/readiness.d.ts.map +1 -0
- package/dist/readiness.js +33 -0
- package/dist/reads.d.ts +11 -0
- package/dist/reads.d.ts.map +1 -0
- package/dist/reads.js +98 -0
- package/dist/recovery.d.ts +25 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +197 -0
- package/dist/refresh.d.ts +13 -2
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +51 -36
- package/dist/replica.d.ts +10 -7
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +131 -79
- package/dist/room-draft.d.ts +41 -9
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +139 -28
- package/dist/rows.d.ts +11 -14
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +87 -50
- package/dist/save.d.ts +32 -3
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +465 -93
- package/dist/search.d.ts +39 -8
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +132 -46
- package/dist/seed.d.ts +16 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +61 -0
- package/dist/shared.d.ts +11 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +368 -0
- package/dist/status.d.ts +6 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +0 -8
- package/dist/store.d.ts +87 -32
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +558 -225
- package/dist/turn.d.ts +7 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +45 -0
- package/dist/wire.d.ts +162 -19
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +82 -67
- package/package.json +31 -7
- package/dist/attachment.d.ts +0 -3
- package/dist/attachment.d.ts.map +0 -1
- package/dist/attachment.js +0 -8
package/dist/connection.js
CHANGED
|
@@ -1,96 +1,121 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { attachedModel } from './attached-model.js';
|
|
2
2
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
3
|
-
import {
|
|
4
|
-
import { filterOf, identityOf, planOf, } from './query.js';
|
|
3
|
+
import { observing, whole } from './observation.js';
|
|
4
|
+
import { filterOf, identityOf, planOf, queryKey, } from './query.js';
|
|
5
5
|
import { createReadSide } from './read-side.js';
|
|
6
|
+
import { seededResult } from './seed.js';
|
|
7
|
+
import { markdownPath, roomOfKey } from './rows.js';
|
|
6
8
|
import { createSearchIndex, searchPlanOf, } from './search.js';
|
|
7
|
-
import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES, } from './adapter.js';
|
|
8
9
|
import { bootstrapRoom } from './bootstrap.js';
|
|
9
|
-
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
10
|
+
import { changeSet, entering, mergeChangeSets } from './change-set.js';
|
|
10
11
|
import { createConversationStore, } from './conversation.js';
|
|
11
|
-
import {
|
|
12
|
+
import { queuedFile } from './change.js';
|
|
13
|
+
import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
|
|
14
|
+
import { runtimeError } from './failure.js';
|
|
12
15
|
import { createLog } from './log.js';
|
|
16
|
+
import { binaryRecord } from './binary.js';
|
|
17
|
+
import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
18
|
+
import { createRecovery } from './recovery.js';
|
|
19
|
+
import { doorReads } from './reads.js';
|
|
13
20
|
import { refreshRoom } from './refresh.js';
|
|
14
|
-
import { buildBatch } from './save.js';
|
|
15
|
-
import { applyReturned, compositionOf,
|
|
16
|
-
import { changedParts, changesOf, held, readParts, returnedInto, underneath, } from './room-draft.js';
|
|
17
|
-
import {
|
|
21
|
+
import { buildBatch, joined, limitedIn, requeued } from './save.js';
|
|
22
|
+
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
|
|
23
|
+
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
24
|
+
import { same, saving, STARTING, transferring, } from './status.js';
|
|
18
25
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
19
|
-
import {
|
|
26
|
+
import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
|
|
27
|
+
import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
|
|
28
|
+
import { FencedMap } from './fence.js';
|
|
29
|
+
import { tally } from './shared.js';
|
|
30
|
+
import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
|
|
31
|
+
import { identityTime, roomDraftRevision } from './identity.js';
|
|
20
32
|
import { beneath, normalize } from './paths.js';
|
|
21
33
|
import { openRuntimeStore, } from './store.js';
|
|
22
34
|
import { unconfirmed } from './transfer.js';
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
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
|
+
}
|
|
27
44
|
function within(root, path) {
|
|
28
45
|
return root === '' || beneath(root, path);
|
|
29
46
|
}
|
|
30
|
-
function declaredIdentity(model) {
|
|
31
|
-
if (model === undefined)
|
|
32
|
-
return false;
|
|
33
|
-
if (model === null)
|
|
34
|
-
return true;
|
|
35
|
-
return (typeof model.identity === 'string' &&
|
|
36
|
-
model.identity.length > 0 &&
|
|
37
|
-
typeof model.version === 'string' &&
|
|
38
|
-
model.version.length > 0);
|
|
39
|
-
}
|
|
40
47
|
function declaredSelection(room) {
|
|
41
48
|
const selection = room.typedSelection;
|
|
42
49
|
if (selection === undefined)
|
|
43
50
|
return true;
|
|
44
51
|
if (!Array.isArray(selection.roots) || selection.roots.length === 0)
|
|
45
52
|
return false;
|
|
46
|
-
if (!declaredIdentity(selection.attachedModel))
|
|
47
|
-
return false;
|
|
48
53
|
return selection.roots.every((root, index) => validRoot(root) &&
|
|
49
54
|
root !== '' &&
|
|
50
55
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
51
56
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return roots.every((root, index) => validRoot(root) &&
|
|
60
|
-
root !== '' &&
|
|
61
|
-
roots.every((other, at) => at === index || !within(other, root)) &&
|
|
62
|
-
selected.every((other) => !within(other, root) && !within(root, other)));
|
|
57
|
+
const OPENING_LANE = 'opening';
|
|
58
|
+
const CONVERSATION_LANE = 'conversations';
|
|
59
|
+
const ROOM_READS = 256;
|
|
60
|
+
const ROOM_ATTEMPTS = 16;
|
|
61
|
+
function operationOf(input) {
|
|
62
|
+
const operation = input?.operation_id;
|
|
63
|
+
return typeof operation === 'string' ? operation : null;
|
|
63
64
|
}
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
function abandonedOn(signal) {
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
if (signal.aborted)
|
|
82
|
+
resolve(null);
|
|
83
|
+
else
|
|
84
|
+
signal.addEventListener('abort', () => resolve(null), { once: true });
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function roomLane(locator) {
|
|
88
|
+
return `room ${locator}`;
|
|
66
89
|
}
|
|
67
90
|
export function composeRuntime(options) {
|
|
68
|
-
const
|
|
91
|
+
const localSaves = options.adapter.localSaves === true;
|
|
69
92
|
let authorization = options.authorization;
|
|
70
93
|
let opening;
|
|
71
94
|
let store;
|
|
72
|
-
const
|
|
95
|
+
const fences = new Map();
|
|
96
|
+
const isFenced = (locator) => fences.has(locator);
|
|
97
|
+
let fencings = 0;
|
|
98
|
+
const lastFenced = new Map();
|
|
99
|
+
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
100
|
+
let discoveries = 0;
|
|
101
|
+
const replicas = new FencedMap(isFenced);
|
|
102
|
+
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
73
103
|
const pendings = new Map();
|
|
74
|
-
const
|
|
104
|
+
const seeds = new Map();
|
|
105
|
+
const seedWatchers = new Map();
|
|
106
|
+
const roomDrafts = new FencedMap(isFenced);
|
|
75
107
|
const presentations = new Map();
|
|
76
|
-
const outcomes = new Map();
|
|
77
|
-
const outcomeRooms = new Map();
|
|
78
|
-
const wanted = new Map();
|
|
79
108
|
let limits = [];
|
|
80
|
-
let slots = [];
|
|
81
109
|
let capabilities = [];
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
? 'provider'
|
|
85
|
-
: 'runtime';
|
|
86
|
-
}
|
|
87
|
-
const rooms = new Map();
|
|
110
|
+
const rooms = new FencedMap(isFenced);
|
|
111
|
+
const counted = new Map();
|
|
88
112
|
const unreadable = new Set();
|
|
89
|
-
const
|
|
90
|
-
const
|
|
113
|
+
const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
|
|
114
|
+
const models = new FencedMap(isModelFenced);
|
|
115
|
+
const liveModels = new FencedMap(isModelFenced);
|
|
91
116
|
const invalidModels = new Set();
|
|
92
|
-
const modelEpoch = new Map();
|
|
93
117
|
const discovered = new Map();
|
|
118
|
+
const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
|
|
94
119
|
let roomsListed = false;
|
|
95
120
|
const scopedRefusals = new Map();
|
|
96
121
|
const sourceRefusals = new Map();
|
|
@@ -118,16 +143,30 @@ export function composeRuntime(options) {
|
|
|
118
143
|
let status = STARTING;
|
|
119
144
|
let shown = STARTING;
|
|
120
145
|
let discoveryPaused = false;
|
|
121
|
-
let timer;
|
|
122
146
|
let closed = false;
|
|
123
147
|
let stopping = false;
|
|
124
|
-
let
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
let
|
|
148
|
+
let withdrawStream;
|
|
149
|
+
const announced = new Map();
|
|
150
|
+
let underway = null;
|
|
151
|
+
let streamed = null;
|
|
152
|
+
let handed = false;
|
|
153
|
+
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
154
|
+
const repairing = new Set();
|
|
155
|
+
const calling = new Set();
|
|
156
|
+
const repairable = new Map();
|
|
157
|
+
let outcomeRetention = null;
|
|
158
|
+
const answering = new Map();
|
|
159
|
+
let complete = false;
|
|
160
|
+
let recorded = false;
|
|
161
|
+
const stale = new Set();
|
|
162
|
+
let generation = 0;
|
|
163
|
+
let discovering = null;
|
|
164
|
+
let opened = null;
|
|
165
|
+
const owed = new Set();
|
|
166
|
+
const lanes = new Map();
|
|
167
|
+
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
168
|
+
const working = new Set();
|
|
128
169
|
let mutations = Promise.resolve();
|
|
129
|
-
let inflight = false;
|
|
130
|
-
let queued = false;
|
|
131
170
|
const roomListeners = new Set();
|
|
132
171
|
const refusalListeners = new Set();
|
|
133
172
|
const storageLossListeners = new Set();
|
|
@@ -137,26 +176,49 @@ export function composeRuntime(options) {
|
|
|
137
176
|
const presentationListeners = new Set();
|
|
138
177
|
const readSide = createReadSide();
|
|
139
178
|
let withdrawConversations;
|
|
140
|
-
let currencyQueued =
|
|
179
|
+
let currencyQueued = null;
|
|
180
|
+
let socketOpen = false;
|
|
141
181
|
let serviceMode = 'open';
|
|
142
182
|
let haltedRefusal = false;
|
|
143
183
|
let openSignals = 0;
|
|
144
184
|
const serviceModeListeners = new Set();
|
|
185
|
+
let member = null;
|
|
186
|
+
const memberListeners = new Set();
|
|
187
|
+
const prepared = new Set();
|
|
188
|
+
const projecting = new Set();
|
|
189
|
+
const unpreparedDemand = new Set();
|
|
190
|
+
const unindexable = new Set();
|
|
191
|
+
let preparing = false;
|
|
192
|
+
let demand = WHOLE_DEMAND;
|
|
193
|
+
let narrowing = null;
|
|
194
|
+
let targets = {};
|
|
195
|
+
let persistence = options.shell.persistence;
|
|
196
|
+
let capacity = null;
|
|
197
|
+
const paused = new Set();
|
|
198
|
+
let rechecking = null;
|
|
199
|
+
const readinessWatches = new Set();
|
|
200
|
+
let readinessQueued = false;
|
|
201
|
+
const catching = new Set();
|
|
202
|
+
const transfers = new FencedMap(isFenced);
|
|
145
203
|
const conversations = createConversationStore({
|
|
146
|
-
|
|
147
|
-
documents: (locator) =>
|
|
204
|
+
agents: declaredAgents,
|
|
205
|
+
documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
|
|
148
206
|
call: (name, input) => invokeAction(name, input),
|
|
149
207
|
durably,
|
|
150
208
|
stream: () => options.adapter.conversations,
|
|
151
209
|
publish: publishChangeSet,
|
|
210
|
+
online: () => !status.providerUnreachable && refusal === null,
|
|
152
211
|
closed: () => closed,
|
|
212
|
+
unanswered: resendLater,
|
|
213
|
+
expired: (operation) => pastHorizon(operation),
|
|
153
214
|
});
|
|
154
215
|
const searchIndex = createSearchIndex({
|
|
155
|
-
|
|
216
|
+
room: (room) => readSide.room(room),
|
|
156
217
|
document: (room, path) => readSide.document(room, path),
|
|
157
|
-
messages: () => conversations.messages(),
|
|
158
|
-
message: (conversation, identity) => conversations.message(conversation, identity),
|
|
159
218
|
});
|
|
219
|
+
const roomsToIndex = new Set();
|
|
220
|
+
const conversationsToIndex = new Set();
|
|
221
|
+
let indexing = false;
|
|
160
222
|
const journal = createLog();
|
|
161
223
|
let notifyReady = () => { };
|
|
162
224
|
const ready = new Promise((resolve) => {
|
|
@@ -220,7 +282,9 @@ export function composeRuntime(options) {
|
|
|
220
282
|
}
|
|
221
283
|
}
|
|
222
284
|
function pending() {
|
|
223
|
-
return
|
|
285
|
+
return (!localSaves &&
|
|
286
|
+
(queuesWithoutReplica.size > 0 ||
|
|
287
|
+
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
224
288
|
}
|
|
225
289
|
function attention() {
|
|
226
290
|
const attentionRequired = [];
|
|
@@ -233,9 +297,16 @@ export function composeRuntime(options) {
|
|
|
233
297
|
returned: structuredClone(record.returned),
|
|
234
298
|
});
|
|
235
299
|
}
|
|
300
|
+
for (const [room, refusal] of sourceRefusals)
|
|
301
|
+
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
236
302
|
attentionRequired.sort((left, right) => {
|
|
237
303
|
if (left.room !== right.room)
|
|
238
304
|
return left.room < right.room ? -1 : 1;
|
|
305
|
+
if ('refusal' in left || 'refusal' in right) {
|
|
306
|
+
if ('refusal' in left && 'refusal' in right)
|
|
307
|
+
return 0;
|
|
308
|
+
return 'refusal' in left ? -1 : 1;
|
|
309
|
+
}
|
|
239
310
|
if (left.path !== right.path)
|
|
240
311
|
return left.path < right.path ? -1 : 1;
|
|
241
312
|
return 0;
|
|
@@ -249,23 +320,30 @@ export function composeRuntime(options) {
|
|
|
249
320
|
}
|
|
250
321
|
return { attentionRequired, reset };
|
|
251
322
|
}
|
|
323
|
+
function documentsIn(locator) {
|
|
324
|
+
const replica = replicas.get(locator);
|
|
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));
|
|
331
|
+
}
|
|
252
332
|
function listed() {
|
|
253
|
-
return structuredClone([...rooms.values()])
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
entries: entries.flatMap(({ locator, segment }) => {
|
|
259
|
-
const room = rooms.get(locator);
|
|
260
|
-
return room === undefined ? [] : [{ room, segment }];
|
|
261
|
-
}),
|
|
262
|
-
})));
|
|
333
|
+
return structuredClone([...rooms.values()].map((room) => {
|
|
334
|
+
const documents = documentsIn(room.locator);
|
|
335
|
+
counted.set(room.locator, documents);
|
|
336
|
+
return { ...room, documents };
|
|
337
|
+
}));
|
|
263
338
|
}
|
|
264
|
-
function roomFacts() {
|
|
265
|
-
const held = [...rooms.values()]
|
|
339
|
+
function roomFacts(reach = null) {
|
|
340
|
+
const held = [...rooms.values()]
|
|
341
|
+
.filter((room) => holds(reach, room.locator))
|
|
342
|
+
.map((room) => {
|
|
343
|
+
const governed = governs(reach, room);
|
|
266
344
|
const attached = [
|
|
267
|
-
modelOf(room.locator),
|
|
268
|
-
...(
|
|
345
|
+
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
346
|
+
...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
|
|
269
347
|
];
|
|
270
348
|
return {
|
|
271
349
|
locator: room.locator,
|
|
@@ -276,18 +354,26 @@ export function composeRuntime(options) {
|
|
|
276
354
|
return { listed: roomsListed, rooms: held };
|
|
277
355
|
}
|
|
278
356
|
function emitRooms() {
|
|
357
|
+
readSide.recover();
|
|
358
|
+
queueReadiness();
|
|
279
359
|
const value = listed();
|
|
280
360
|
for (const listener of roomListeners)
|
|
281
361
|
listener(value);
|
|
282
362
|
}
|
|
283
363
|
function emitRefusal() {
|
|
364
|
+
queueReadiness();
|
|
284
365
|
for (const listener of refusalListeners)
|
|
285
366
|
listener(refusal);
|
|
286
367
|
}
|
|
287
368
|
function emitStorageLoss() {
|
|
369
|
+
queueReadiness();
|
|
288
370
|
for (const listener of storageLossListeners)
|
|
289
371
|
listener(storageLoss);
|
|
290
372
|
}
|
|
373
|
+
function emitMember() {
|
|
374
|
+
for (const listener of memberListeners)
|
|
375
|
+
listener(member);
|
|
376
|
+
}
|
|
291
377
|
function moveTo(next) {
|
|
292
378
|
if (closed)
|
|
293
379
|
return;
|
|
@@ -297,7 +383,8 @@ export function composeRuntime(options) {
|
|
|
297
383
|
function show() {
|
|
298
384
|
if (closed)
|
|
299
385
|
return;
|
|
300
|
-
|
|
386
|
+
queueReadiness();
|
|
387
|
+
const next = status.state === 'synced' && demand.conversations && !conversations.current()
|
|
301
388
|
? { ...status, state: 'syncing' }
|
|
302
389
|
: status;
|
|
303
390
|
if (same(shown, next))
|
|
@@ -306,14 +393,17 @@ export function composeRuntime(options) {
|
|
|
306
393
|
for (const listener of statusListeners)
|
|
307
394
|
listener(shown);
|
|
308
395
|
}
|
|
309
|
-
function
|
|
310
|
-
|
|
396
|
+
function entryOf(batch) {
|
|
397
|
+
if (localSaves)
|
|
398
|
+
return Object.freeze({ batch, state: 'local' });
|
|
399
|
+
const state = pendings.get(batch.id);
|
|
400
|
+
if (state?.accepted !== undefined) {
|
|
401
|
+
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
402
|
+
}
|
|
403
|
+
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
311
404
|
}
|
|
312
405
|
function entriesOf(locator) {
|
|
313
|
-
|
|
314
|
-
if (replica === undefined)
|
|
315
|
-
return [];
|
|
316
|
-
return replica.queue.map((batch) => Object.freeze({ batch, ...submissionOf(batch) }));
|
|
406
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
317
407
|
}
|
|
318
408
|
function emitQueue(locator) {
|
|
319
409
|
const entries = entriesOf(locator);
|
|
@@ -323,12 +413,524 @@ export function composeRuntime(options) {
|
|
|
323
413
|
}
|
|
324
414
|
}
|
|
325
415
|
function applyChangeSet(set) {
|
|
326
|
-
|
|
327
|
-
|
|
416
|
+
const held = {
|
|
417
|
+
...set,
|
|
418
|
+
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
419
|
+
};
|
|
420
|
+
readSide.apply(held);
|
|
421
|
+
searchIndex.changed(held);
|
|
422
|
+
}
|
|
423
|
+
async function referenced(locator) {
|
|
424
|
+
for (;;) {
|
|
425
|
+
const record = replicas.get(locator)?.confirmed.record;
|
|
426
|
+
if (record === undefined || closed)
|
|
427
|
+
return;
|
|
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
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async function prepareReplica(locator) {
|
|
436
|
+
await referenced(locator);
|
|
437
|
+
const replica = replicas.get(locator);
|
|
438
|
+
if (replica === undefined || closed || prepared.has(locator))
|
|
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)
|
|
456
|
+
return;
|
|
457
|
+
prepared.add(locator);
|
|
458
|
+
}
|
|
459
|
+
finally {
|
|
460
|
+
projecting.delete(locator);
|
|
461
|
+
}
|
|
462
|
+
conversations.changed(mergeChangeSets(entered));
|
|
463
|
+
readSide.recover();
|
|
464
|
+
queueReadiness();
|
|
465
|
+
}
|
|
466
|
+
function unprepared() {
|
|
467
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
|
|
468
|
+
}
|
|
469
|
+
function unbuilt(locator, error) {
|
|
470
|
+
unindexable.add(locator);
|
|
471
|
+
journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
472
|
+
queueReadiness();
|
|
473
|
+
}
|
|
474
|
+
function prepare(locators = []) {
|
|
475
|
+
for (const locator of locators)
|
|
476
|
+
unpreparedDemand.add(locator);
|
|
477
|
+
if (preparing)
|
|
478
|
+
return;
|
|
479
|
+
preparing = true;
|
|
480
|
+
void (async () => {
|
|
481
|
+
try {
|
|
482
|
+
for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
|
|
483
|
+
await yielded();
|
|
484
|
+
if (closed)
|
|
485
|
+
return;
|
|
486
|
+
unpreparedDemand.delete(waiting);
|
|
487
|
+
await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
finally {
|
|
491
|
+
preparing = false;
|
|
492
|
+
}
|
|
493
|
+
})();
|
|
494
|
+
}
|
|
495
|
+
function reachedRooms(plan) {
|
|
496
|
+
const reach = plan.reach;
|
|
497
|
+
const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
498
|
+
const named = plan.rooms ??
|
|
499
|
+
(reached === null
|
|
500
|
+
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
501
|
+
: [...reached]);
|
|
502
|
+
return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
|
|
503
|
+
}
|
|
504
|
+
function awaited(locator) {
|
|
505
|
+
return (!roomsListed ||
|
|
506
|
+
(rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
|
|
507
|
+
}
|
|
508
|
+
function covered(plan) {
|
|
509
|
+
const locators = reachedRooms(plan);
|
|
510
|
+
if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
|
|
511
|
+
return { pending: [] };
|
|
512
|
+
}
|
|
513
|
+
const pending = [];
|
|
514
|
+
const coverage = [];
|
|
515
|
+
const overlay = [];
|
|
516
|
+
for (const locator of locators) {
|
|
517
|
+
const replica = replicas.get(locator);
|
|
518
|
+
if (replica === undefined) {
|
|
519
|
+
if (awaited(locator))
|
|
520
|
+
pending.push({ room: locator, awaits: 'acquisition' });
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (!prepared.has(locator)) {
|
|
524
|
+
pending.push({ room: locator, awaits: 'preparation' });
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
coverage.push({
|
|
528
|
+
room: locator,
|
|
529
|
+
generation: replica.confirmed.record.transfer,
|
|
530
|
+
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
531
|
+
});
|
|
532
|
+
for (const batch of replica.queue)
|
|
533
|
+
overlay.push(batch.id);
|
|
534
|
+
}
|
|
535
|
+
if (pending.length > 0)
|
|
536
|
+
return { pending };
|
|
537
|
+
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
538
|
+
}
|
|
539
|
+
function awaitPreparation(cover) {
|
|
540
|
+
if (!('pending' in cover))
|
|
541
|
+
return;
|
|
542
|
+
const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
|
|
543
|
+
if (waiting.length > 0)
|
|
544
|
+
prepare(waiting.map(({ room }) => room));
|
|
545
|
+
}
|
|
546
|
+
function acquires(room) {
|
|
547
|
+
return (demanded(demand, room) ||
|
|
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 heldLocators(acquired) {
|
|
558
|
+
return demand.whole ? acquired : [...new Set([...acquired, ...demand.rooms])].sort();
|
|
559
|
+
}
|
|
560
|
+
function heldDemand(acquired) {
|
|
561
|
+
if (demand.whole)
|
|
562
|
+
return null;
|
|
563
|
+
return {
|
|
564
|
+
rooms: heldLocators(acquired.map((room) => room.locator)),
|
|
565
|
+
conversations: demand.conversations,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function applyDemand(next) {
|
|
569
|
+
const before = heldLocators(acquiredLocators());
|
|
570
|
+
const { whole, conversations: conversed } = demand;
|
|
571
|
+
demand = next;
|
|
572
|
+
const after = heldLocators(acquiredLocators());
|
|
573
|
+
queueReadiness();
|
|
574
|
+
if (!conversed && next.conversations)
|
|
575
|
+
scheduleCurrency();
|
|
576
|
+
if (JSON.stringify(before) === JSON.stringify(after) &&
|
|
577
|
+
whole === next.whole &&
|
|
578
|
+
conversed === next.conversations)
|
|
579
|
+
return;
|
|
580
|
+
journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
|
|
581
|
+
if (options.adapter.roomStream === undefined)
|
|
582
|
+
void sync('unstreamed');
|
|
583
|
+
else if (socketOpen)
|
|
584
|
+
options.adapter.roomStream.reopen();
|
|
585
|
+
}
|
|
586
|
+
function acquire(next) {
|
|
587
|
+
if (closed)
|
|
588
|
+
return;
|
|
589
|
+
if (narrowing !== null)
|
|
590
|
+
clearTimeout(narrowing);
|
|
591
|
+
narrowing = null;
|
|
592
|
+
const widening = wider(next, demand);
|
|
593
|
+
if (!widening && !narrower(next, demand))
|
|
594
|
+
return;
|
|
595
|
+
if (widening || !roomsListed) {
|
|
596
|
+
applyDemand(next);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
// A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
|
|
600
|
+
narrowing = setTimeout(() => {
|
|
601
|
+
narrowing = null;
|
|
602
|
+
applyDemand(next);
|
|
603
|
+
}, 5_000);
|
|
604
|
+
}
|
|
605
|
+
function requiredRooms(reach) {
|
|
606
|
+
if (reach === null)
|
|
607
|
+
return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
|
|
608
|
+
const named = [...new Set(reach.rooms)].sort();
|
|
609
|
+
if (roomsListed)
|
|
610
|
+
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
611
|
+
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
612
|
+
}
|
|
613
|
+
function roomState(locator, search) {
|
|
614
|
+
if (sourceRefusals.has(locator))
|
|
615
|
+
return 'refused';
|
|
616
|
+
const listedRoom = discovered.get(locator);
|
|
617
|
+
if (roomsListed &&
|
|
618
|
+
(listedRoom === undefined ||
|
|
619
|
+
listedRoom.replica.admission !== 'admitted' ||
|
|
620
|
+
unreadable.has(locator)))
|
|
621
|
+
return 'unadmitted';
|
|
622
|
+
if (!replicas.has(locator))
|
|
623
|
+
return 'acquiring';
|
|
624
|
+
if (!prepared.has(locator))
|
|
625
|
+
return 'preparing';
|
|
626
|
+
if (search && !searchIndex.rooms().has(locator))
|
|
627
|
+
return 'indexing';
|
|
628
|
+
return 'complete';
|
|
629
|
+
}
|
|
630
|
+
function conversationCoverage(reach, search) {
|
|
631
|
+
const declared = reach === null || reach.conversations;
|
|
632
|
+
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
633
|
+
return 'undeclared';
|
|
634
|
+
if (!conversations.known() || !conversations.current())
|
|
635
|
+
return 'acquiring';
|
|
636
|
+
if (!search)
|
|
637
|
+
return 'complete';
|
|
638
|
+
const unindexed = conversations
|
|
639
|
+
.searchable()
|
|
640
|
+
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
641
|
+
if (unindexed.length === 0)
|
|
642
|
+
return 'complete';
|
|
643
|
+
index([], unindexed);
|
|
644
|
+
return 'indexing';
|
|
645
|
+
}
|
|
646
|
+
function connectionOf() {
|
|
647
|
+
if (socketOpen)
|
|
648
|
+
return 'connected';
|
|
649
|
+
if (status.providerUnreachable)
|
|
650
|
+
return 'unreachable';
|
|
651
|
+
return options.adapter.roomStream === undefined && status.lastCompleted !== null
|
|
652
|
+
? 'connected'
|
|
653
|
+
: 'connecting';
|
|
654
|
+
}
|
|
655
|
+
function refusedIn(locators) {
|
|
656
|
+
return locators.flatMap((room) => {
|
|
657
|
+
const held = sourceRefusals.get(room);
|
|
658
|
+
if (held === undefined)
|
|
659
|
+
return [];
|
|
660
|
+
const { file, location } = held.fields;
|
|
661
|
+
return [
|
|
662
|
+
{
|
|
663
|
+
room,
|
|
664
|
+
file: typeof file === 'string' ? file : '',
|
|
665
|
+
rule: held.code,
|
|
666
|
+
location: typeof location === 'string' ? location : null,
|
|
667
|
+
},
|
|
668
|
+
];
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
function readinessFor(port, reach) {
|
|
672
|
+
const required = requiredRooms(reach);
|
|
673
|
+
const search = port.search === 'required';
|
|
674
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
|
|
675
|
+
const building = (state) => coverage
|
|
676
|
+
.filter((room) => room.state === state && !unindexable.has(room.room))
|
|
677
|
+
.map(({ room }) => room);
|
|
678
|
+
const preparing = building('preparing');
|
|
679
|
+
if (preparing.length > 0)
|
|
680
|
+
prepare(preparing);
|
|
681
|
+
const indexing = building('indexing');
|
|
682
|
+
if (indexing.length > 0)
|
|
683
|
+
index(indexing, []);
|
|
684
|
+
return readinessOf({
|
|
685
|
+
code: port.code,
|
|
686
|
+
coverage: {
|
|
687
|
+
listed: required.listed,
|
|
688
|
+
rooms: coverage,
|
|
689
|
+
conversations: conversationCoverage(reach, search),
|
|
690
|
+
search: search ? 'required' : 'unused',
|
|
691
|
+
},
|
|
692
|
+
persistence,
|
|
693
|
+
connection: connectionOf(),
|
|
694
|
+
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
695
|
+
capacity: capacity !== null &&
|
|
696
|
+
coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
|
|
697
|
+
? capacity
|
|
698
|
+
: port.capacity,
|
|
699
|
+
refused: refusedIn(required.rooms),
|
|
700
|
+
lost: storageLoss !== null,
|
|
701
|
+
established: targets[port.target]?.established === true,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
function establish(name) {
|
|
705
|
+
targets = { ...targets, [name]: { established: true } };
|
|
706
|
+
queueReadiness();
|
|
707
|
+
void durably((local) => local.keepTargets(targets)).catch(() => {
|
|
708
|
+
journal.record(`target record failed ${name}`);
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
function deliverReadiness(watch) {
|
|
712
|
+
if (!readinessWatches.has(watch))
|
|
713
|
+
return;
|
|
714
|
+
const readiness = readinessFor(watch.port, watch.reach);
|
|
715
|
+
if (readiness.ready && targets[watch.port.target]?.established !== true) {
|
|
716
|
+
establish(watch.port.target);
|
|
717
|
+
}
|
|
718
|
+
const shown = JSON.stringify(readiness);
|
|
719
|
+
if (shown === watch.last)
|
|
720
|
+
return;
|
|
721
|
+
watch.last = shown;
|
|
722
|
+
watch.deliver(structuredClone(readiness));
|
|
723
|
+
}
|
|
724
|
+
function queueReadiness() {
|
|
725
|
+
if (readinessQueued || closed || readinessWatches.size === 0)
|
|
726
|
+
return;
|
|
727
|
+
readinessQueued = true;
|
|
728
|
+
queueMicrotask(() => {
|
|
729
|
+
readinessQueued = false;
|
|
730
|
+
if (closed)
|
|
731
|
+
return;
|
|
732
|
+
for (const watch of [...readinessWatches])
|
|
733
|
+
deliverReadiness(watch);
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
function watchReadiness(port, reach, deliver) {
|
|
737
|
+
const watch = { port, reach, deliver, last: null };
|
|
738
|
+
readinessWatches.add(watch);
|
|
739
|
+
void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
|
|
740
|
+
return () => {
|
|
741
|
+
readinessWatches.delete(watch);
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function layerBytes(layer) {
|
|
745
|
+
let bytes = 0;
|
|
746
|
+
for (const [path, text] of Object.entries(layer.documents))
|
|
747
|
+
bytes += (path.length + text.length) * 2;
|
|
748
|
+
return bytes;
|
|
749
|
+
}
|
|
750
|
+
function grownBy(next, previous) {
|
|
751
|
+
if (previous === undefined)
|
|
752
|
+
return layerBytes(next);
|
|
753
|
+
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
754
|
+
return layerBytes({ ...next, documents });
|
|
755
|
+
}
|
|
756
|
+
function exceeded(error) {
|
|
757
|
+
return (typeof error === 'object' &&
|
|
758
|
+
error !== null &&
|
|
759
|
+
error.name === 'QuotaExceededError');
|
|
760
|
+
}
|
|
761
|
+
async function estimated() {
|
|
762
|
+
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
763
|
+
return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
|
|
764
|
+
}
|
|
765
|
+
function pause(locator, cause) {
|
|
766
|
+
paused.add(locator);
|
|
767
|
+
capacity = cause;
|
|
768
|
+
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
769
|
+
queueReadiness();
|
|
770
|
+
if (rechecking !== null)
|
|
771
|
+
return;
|
|
772
|
+
// A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
|
|
773
|
+
rechecking = setInterval(() => {
|
|
774
|
+
void recheck();
|
|
775
|
+
}, 60_000);
|
|
776
|
+
}
|
|
777
|
+
async function recheck() {
|
|
778
|
+
const held = capacity;
|
|
779
|
+
if (held === null || closed)
|
|
780
|
+
return;
|
|
781
|
+
const available = await estimated().catch(() => null);
|
|
782
|
+
if (available === null || available < (held.needed ?? 0))
|
|
783
|
+
return;
|
|
784
|
+
if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
|
|
785
|
+
return;
|
|
786
|
+
resume();
|
|
787
|
+
}
|
|
788
|
+
function resume() {
|
|
789
|
+
if (rechecking !== null)
|
|
790
|
+
clearInterval(rechecking);
|
|
791
|
+
rechecking = null;
|
|
792
|
+
if (paused.size === 0 && capacity === null)
|
|
793
|
+
return;
|
|
794
|
+
paused.clear();
|
|
795
|
+
capacity = null;
|
|
796
|
+
journal.record('capacity resumed');
|
|
797
|
+
queueReadiness();
|
|
798
|
+
if (!closed && authorization !== undefined)
|
|
799
|
+
void sync('unstreamed');
|
|
800
|
+
}
|
|
801
|
+
async function reclaimed(local) {
|
|
802
|
+
const unreferenced = [...transfers.keys()].filter((locator) => {
|
|
803
|
+
const room = discovered.get(locator);
|
|
804
|
+
return room === undefined || !acquires(room);
|
|
805
|
+
});
|
|
806
|
+
for (const locator of unreferenced) {
|
|
807
|
+
await local.abandon(locator);
|
|
808
|
+
transfers.delete(locator);
|
|
809
|
+
}
|
|
810
|
+
if (unreferenced.length > 0)
|
|
811
|
+
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
812
|
+
return unreferenced.length > 0;
|
|
813
|
+
}
|
|
814
|
+
async function admitted(local, locator, needed) {
|
|
815
|
+
if (paused.has(locator))
|
|
816
|
+
return false;
|
|
817
|
+
let available = await estimated().catch(() => null);
|
|
818
|
+
if (available === null || available >= needed)
|
|
819
|
+
return true;
|
|
820
|
+
if (await reclaimed(local))
|
|
821
|
+
available = await estimated().catch(() => null);
|
|
822
|
+
if (available === null || available >= needed)
|
|
823
|
+
return true;
|
|
824
|
+
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
async function exceededOn(locator, bytes) {
|
|
828
|
+
const available = await estimated().catch(() => null);
|
|
829
|
+
pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
|
|
830
|
+
}
|
|
831
|
+
function seedRoomHeld(room, version) {
|
|
832
|
+
return prepared.has(room) && reachedBy(room, version);
|
|
833
|
+
}
|
|
834
|
+
function seedHeld(at) {
|
|
835
|
+
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
836
|
+
}
|
|
837
|
+
function seedApplies(plan) {
|
|
838
|
+
const seed = seeds.get(plan.key);
|
|
839
|
+
if (seed === undefined)
|
|
840
|
+
return null;
|
|
841
|
+
if (!seedHeld(seed.at))
|
|
842
|
+
return seed;
|
|
843
|
+
seeds.delete(plan.key);
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
function seedBehind(plan, cover) {
|
|
847
|
+
const seed = seedApplies(plan);
|
|
848
|
+
if (seed === null || 'pending' in cover)
|
|
849
|
+
return cover;
|
|
850
|
+
return {
|
|
851
|
+
pending: Object.entries(seed.at)
|
|
852
|
+
.filter(([room, version]) => !seedRoomHeld(room, version))
|
|
853
|
+
.map(([room]) => ({ room, awaits: 'acquisition' })),
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
function seed(entry) {
|
|
857
|
+
if (seedHeld(entry.at))
|
|
858
|
+
return;
|
|
859
|
+
for (const { query, rows } of entry.queries) {
|
|
860
|
+
const key = queryKey(query);
|
|
861
|
+
seeds.set(key, { rows, at: entry.at });
|
|
862
|
+
for (const shown of seedWatchers.get(key) ?? [])
|
|
863
|
+
shown();
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
async function rowsQuery(plan) {
|
|
867
|
+
const before = covered(plan);
|
|
868
|
+
awaitPreparation(before);
|
|
869
|
+
const seeded = seedApplies(plan);
|
|
870
|
+
if (seeded !== null)
|
|
871
|
+
return seededResult(seeded).result;
|
|
872
|
+
if ('pending' in before)
|
|
873
|
+
return before;
|
|
874
|
+
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
875
|
+
awaitPreparation(cover);
|
|
876
|
+
if ('pending' in cover)
|
|
877
|
+
return cover;
|
|
878
|
+
return 'groups' in rows
|
|
879
|
+
? { groups: rows.groups, coverage: cover.coverage }
|
|
880
|
+
: { documents: rows.documents, coverage: cover.coverage };
|
|
881
|
+
}
|
|
882
|
+
async function indexConversation(conversation) {
|
|
883
|
+
searchIndex.indexMessages(conversation, [], false);
|
|
884
|
+
let after = null;
|
|
885
|
+
for (;;) {
|
|
886
|
+
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
887
|
+
if (closed)
|
|
888
|
+
return;
|
|
889
|
+
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
890
|
+
if (batch.length === 0)
|
|
891
|
+
return;
|
|
892
|
+
after = batch[batch.length - 1].identity;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
function indexNext() {
|
|
896
|
+
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
897
|
+
if (room !== undefined) {
|
|
898
|
+
roomsToIndex.delete(room);
|
|
899
|
+
return () => prepared.has(room)
|
|
900
|
+
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
901
|
+
: undefined;
|
|
902
|
+
}
|
|
903
|
+
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
904
|
+
if (conversation === undefined)
|
|
905
|
+
return null;
|
|
906
|
+
conversationsToIndex.delete(conversation);
|
|
907
|
+
return () => indexConversation(conversation);
|
|
908
|
+
}
|
|
909
|
+
function index(rooms, named) {
|
|
910
|
+
for (const room of rooms)
|
|
911
|
+
roomsToIndex.add(room);
|
|
912
|
+
for (const conversation of named)
|
|
913
|
+
conversationsToIndex.add(conversation);
|
|
914
|
+
if (indexing)
|
|
915
|
+
return;
|
|
916
|
+
indexing = true;
|
|
917
|
+
void (async () => {
|
|
918
|
+
try {
|
|
919
|
+
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
920
|
+
await yielded();
|
|
921
|
+
await unit();
|
|
922
|
+
queueReadiness();
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
finally {
|
|
926
|
+
indexing = false;
|
|
927
|
+
}
|
|
928
|
+
})().catch(() => { });
|
|
328
929
|
}
|
|
329
930
|
function publishChangeSet(set) {
|
|
330
931
|
applyChangeSet(set);
|
|
331
|
-
if (set.rooms.length > 0
|
|
932
|
+
if (set.rooms.length > 0 ||
|
|
933
|
+
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
332
934
|
emitRooms();
|
|
333
935
|
for (const locator of set.queues)
|
|
334
936
|
emitQueue(locator);
|
|
@@ -339,26 +941,8 @@ export function composeRuntime(options) {
|
|
|
339
941
|
for (const app of set.presentations)
|
|
340
942
|
emitPresentation(app);
|
|
341
943
|
conversations.changed(set);
|
|
342
|
-
const cards = declaredCards();
|
|
343
|
-
if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
|
|
344
|
-
scheduleCurrency();
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
function stopTimer() {
|
|
348
|
-
if (timer === undefined)
|
|
349
|
-
return;
|
|
350
|
-
clearInterval(timer);
|
|
351
|
-
timer = undefined;
|
|
352
|
-
}
|
|
353
|
-
function startTimer() {
|
|
354
|
-
if (timer !== undefined)
|
|
355
|
-
return;
|
|
356
|
-
timer = setInterval(() => {
|
|
357
|
-
void enqueue();
|
|
358
|
-
}, period);
|
|
359
944
|
}
|
|
360
945
|
async function refuse(reason, provider = null) {
|
|
361
|
-
stopTimer();
|
|
362
946
|
discoveryPaused = true;
|
|
363
947
|
try {
|
|
364
948
|
if (reason === 'revoked-authorization') {
|
|
@@ -366,10 +950,7 @@ export function composeRuntime(options) {
|
|
|
366
950
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
367
951
|
try {
|
|
368
952
|
const local = await openLocal();
|
|
369
|
-
await local.revokeModels([...replicas.values()].map((replica) =>
|
|
370
|
-
record: replica.confirmed.record,
|
|
371
|
-
queue: replica.queue,
|
|
372
|
-
})));
|
|
953
|
+
await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
|
|
373
954
|
}
|
|
374
955
|
catch {
|
|
375
956
|
journal.record('model revocation persistence failed');
|
|
@@ -381,7 +962,6 @@ export function composeRuntime(options) {
|
|
|
381
962
|
}
|
|
382
963
|
for (const key of keys) {
|
|
383
964
|
invalidModels.add(key);
|
|
384
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
385
965
|
models.set(key, revoked);
|
|
386
966
|
liveModels.delete(key);
|
|
387
967
|
}
|
|
@@ -394,10 +974,7 @@ export function composeRuntime(options) {
|
|
|
394
974
|
model: revoked,
|
|
395
975
|
selectionModel: revoked,
|
|
396
976
|
};
|
|
397
|
-
|
|
398
|
-
replicas.set(locator, read.kind === 'staged'
|
|
399
|
-
? replicaOf(read.confirmed, before.queue, rewriting(), before)
|
|
400
|
-
: { ...before, confirmed: { ...before.confirmed, record } });
|
|
977
|
+
replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
|
|
401
978
|
}
|
|
402
979
|
rooms.set(locator, {
|
|
403
980
|
...room,
|
|
@@ -414,7 +991,8 @@ export function composeRuntime(options) {
|
|
|
414
991
|
}
|
|
415
992
|
finally {
|
|
416
993
|
refusal = {
|
|
417
|
-
reason,
|
|
994
|
+
code: reason,
|
|
995
|
+
retryable: false,
|
|
418
996
|
reauthorizationOffered: reason === 'revoked-authorization',
|
|
419
997
|
refusal: provider,
|
|
420
998
|
};
|
|
@@ -427,36 +1005,43 @@ export function composeRuntime(options) {
|
|
|
427
1005
|
rooms.set(room.locator, {
|
|
428
1006
|
name: room.name,
|
|
429
1007
|
locator: room.locator,
|
|
1008
|
+
handle: room.handle,
|
|
430
1009
|
operations: room.operations,
|
|
431
1010
|
replica: room.replica,
|
|
432
|
-
attachedModel: room.attachedModel,
|
|
433
1011
|
model: modelTypes(modelOf(room.locator)),
|
|
434
1012
|
typedSelection: selection === null
|
|
435
1013
|
? null
|
|
436
1014
|
: {
|
|
437
1015
|
roots: selection.roots,
|
|
438
|
-
attachedModel: selection.attachedModel,
|
|
439
1016
|
model: modelTypes(modelOf(room.locator, 'typed-selection')),
|
|
440
1017
|
},
|
|
441
|
-
untypedFolders: room.untypedFolders ?? [],
|
|
442
1018
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
443
1019
|
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
444
|
-
attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
|
|
445
1020
|
synchronizationAvailable,
|
|
446
1021
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
447
1022
|
});
|
|
448
1023
|
}
|
|
449
|
-
async function applyDiscovery(
|
|
1024
|
+
async function applyDiscovery(found, started) {
|
|
1025
|
+
for (const room of found) {
|
|
1026
|
+
const fence = fences.get(room.locator);
|
|
1027
|
+
if (fence !== undefined && fence.erased && fence.at < started)
|
|
1028
|
+
fences.delete(room.locator);
|
|
1029
|
+
}
|
|
1030
|
+
await loseRooms(found
|
|
1031
|
+
.filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
|
|
1032
|
+
.map((room) => room.locator));
|
|
1033
|
+
const listed = found.filter((room) => !fences.has(room.locator));
|
|
450
1034
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
451
1035
|
for (const locator of listedLocators)
|
|
452
1036
|
scopedRefusals.delete(locator);
|
|
453
1037
|
discovered.clear();
|
|
454
1038
|
for (const room of listed)
|
|
455
1039
|
discovered.set(room.locator, room);
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
1040
|
+
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
|
|
1041
|
+
if (options.adapter.exhaustiveDiscovery === true)
|
|
1042
|
+
await loseRooms(unlisted);
|
|
1043
|
+
else
|
|
1044
|
+
unlistedKept(unlisted);
|
|
460
1045
|
for (const room of listed) {
|
|
461
1046
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
462
1047
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -467,56 +1052,97 @@ export function composeRuntime(options) {
|
|
|
467
1052
|
}
|
|
468
1053
|
roomsListed = true;
|
|
469
1054
|
emitRooms();
|
|
1055
|
+
return listed;
|
|
470
1056
|
}
|
|
471
|
-
function
|
|
472
|
-
return
|
|
1057
|
+
function heldLocally(locator) {
|
|
1058
|
+
return (replicas.has(locator) ||
|
|
1059
|
+
roomDrafts.has(locator) ||
|
|
1060
|
+
transfers.has(locator) ||
|
|
1061
|
+
queuesWithoutReplica.has(locator));
|
|
473
1062
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}));
|
|
491
|
-
for (const locator of unlisted) {
|
|
492
|
-
for (const batch of replicas.get(locator)?.queue ?? [])
|
|
493
|
-
pendings.delete(batch.id);
|
|
494
|
-
replicas.delete(locator);
|
|
1063
|
+
function unlistedHeld(listed) {
|
|
1064
|
+
return [
|
|
1065
|
+
...new Set([
|
|
1066
|
+
...rooms.keys(),
|
|
1067
|
+
...roomDrafts.keys(),
|
|
1068
|
+
...transfers.keys(),
|
|
1069
|
+
...queuesWithoutReplica.keys(),
|
|
1070
|
+
]),
|
|
1071
|
+
].filter((locator) => !listed.has(locator));
|
|
1072
|
+
}
|
|
1073
|
+
function unlistedKept(unlisted) {
|
|
1074
|
+
for (const locator of unlisted) {
|
|
1075
|
+
const held = replicas.get(locator)?.confirmed.record;
|
|
1076
|
+
if (held !== undefined)
|
|
1077
|
+
list(held, false);
|
|
1078
|
+
else
|
|
495
1079
|
rooms.delete(locator);
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
function fence(locators) {
|
|
1083
|
+
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1084
|
+
const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
1085
|
+
room: true,
|
|
1086
|
+
queue: true,
|
|
1087
|
+
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
1088
|
+
}));
|
|
1089
|
+
for (const locator of fresh) {
|
|
1090
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1091
|
+
lastFenced.set(locator, ++fencings);
|
|
1092
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1093
|
+
pendings.delete(batch.id);
|
|
1094
|
+
replicas.delete(locator);
|
|
1095
|
+
queuesWithoutReplica.delete(locator);
|
|
1096
|
+
rooms.delete(locator);
|
|
1097
|
+
roomDrafts.delete(locator);
|
|
1098
|
+
transfers.delete(locator);
|
|
1099
|
+
catching.delete(locator);
|
|
1100
|
+
discovered.delete(locator);
|
|
1101
|
+
announced.delete(locator);
|
|
1102
|
+
unreadable.delete(locator);
|
|
1103
|
+
stale.delete(locator);
|
|
1104
|
+
repairable.delete(locator);
|
|
1105
|
+
unindexable.delete(locator);
|
|
1106
|
+
paused.delete(locator);
|
|
1107
|
+
scopedRefusals.delete(locator);
|
|
1108
|
+
sourceRefusals.delete(locator);
|
|
1109
|
+
declarationRefusals.delete(locator);
|
|
1110
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
1111
|
+
const key = modelKey(locator, attached);
|
|
1112
|
+
models.delete(key);
|
|
1113
|
+
liveModels.delete(key);
|
|
1114
|
+
invalidModels.delete(key);
|
|
509
1115
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
1116
|
+
journal.record(`room lost ${locator}`);
|
|
1117
|
+
}
|
|
1118
|
+
if (fresh.length === 0)
|
|
1119
|
+
return fresh;
|
|
1120
|
+
publishChangeSet(mergeChangeSets(removed));
|
|
1121
|
+
for (const locator of fresh)
|
|
1122
|
+
prepared.delete(locator);
|
|
1123
|
+
searchIndex.forget(fresh);
|
|
1124
|
+
readSide.recover();
|
|
1125
|
+
moveTo({ ...status, ...attention() });
|
|
1126
|
+
return fresh;
|
|
1127
|
+
}
|
|
1128
|
+
async function erased(local, locators) {
|
|
1129
|
+
await local.erase(locators);
|
|
1130
|
+
for (const locator of locators) {
|
|
1131
|
+
const held = fences.get(locator);
|
|
1132
|
+
if (held !== undefined)
|
|
1133
|
+
held.erased = true;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
async function loseRooms(locators) {
|
|
1137
|
+
const fresh = fence(locators);
|
|
1138
|
+
if (fresh.length > 0)
|
|
1139
|
+
await durably((local) => erased(local, fresh));
|
|
513
1140
|
}
|
|
514
1141
|
function restored(key, persisted) {
|
|
515
1142
|
const prior = models.get(key);
|
|
516
1143
|
if (prior !== undefined &&
|
|
517
1144
|
prior.status !== 'unavailable' &&
|
|
518
1145
|
(persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
|
|
519
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
520
1146
|
invalidModels.add(key);
|
|
521
1147
|
}
|
|
522
1148
|
const model = persisted.status === 'unavailable' ||
|
|
@@ -531,10 +1157,9 @@ export function composeRuntime(options) {
|
|
|
531
1157
|
return model;
|
|
532
1158
|
}
|
|
533
1159
|
async function hydrate(opened) {
|
|
534
|
-
const [records, queues,
|
|
1160
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
|
|
535
1161
|
opened.all(),
|
|
536
1162
|
opened.queues(),
|
|
537
|
-
opened.outcomes(),
|
|
538
1163
|
opened.roomDrafts(),
|
|
539
1164
|
opened.presentation(),
|
|
540
1165
|
opened.conversations(),
|
|
@@ -542,11 +1167,17 @@ export function composeRuntime(options) {
|
|
|
542
1167
|
opened.lastCompleted(),
|
|
543
1168
|
opened.providerLimits(),
|
|
544
1169
|
opened.capabilities(),
|
|
545
|
-
opened.
|
|
1170
|
+
opened.member(),
|
|
1171
|
+
opened.transfers(),
|
|
1172
|
+
opened.targets(),
|
|
546
1173
|
]);
|
|
1174
|
+
targets = heldTargets;
|
|
1175
|
+
transfers.clear();
|
|
1176
|
+
for (const record of staged)
|
|
1177
|
+
transfers.set(record.locator, record);
|
|
547
1178
|
limits = declared;
|
|
548
1179
|
capabilities = named;
|
|
549
|
-
|
|
1180
|
+
member = known;
|
|
550
1181
|
roomDrafts.clear();
|
|
551
1182
|
for (const record of kept)
|
|
552
1183
|
rememberRoomDraft(record);
|
|
@@ -560,27 +1191,20 @@ export function composeRuntime(options) {
|
|
|
560
1191
|
for (const { batch, state } of queue.pending)
|
|
561
1192
|
pendings.set(batch.id, state);
|
|
562
1193
|
}
|
|
563
|
-
outcomes.clear();
|
|
564
|
-
outcomeRooms.clear();
|
|
565
|
-
for (const record of recordedOutcomes) {
|
|
566
|
-
outcomes.set(record.id, record.outcome);
|
|
567
|
-
outcomeRooms.set(record.id, record.locator);
|
|
568
|
-
}
|
|
569
1194
|
const previousReplicas = new Map(replicas);
|
|
570
1195
|
replicas.clear();
|
|
1196
|
+
queuesWithoutReplica.clear();
|
|
1197
|
+
const replicated = new Set(records.map((record) => record.locator));
|
|
1198
|
+
for (const [locator, queue] of queuedBatches) {
|
|
1199
|
+
if (!replicated.has(locator))
|
|
1200
|
+
queuesWithoutReplica.set(locator, queue);
|
|
1201
|
+
}
|
|
571
1202
|
for (const record of records) {
|
|
572
1203
|
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
573
1204
|
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
574
1205
|
const prior = previousReplicas.get(record.locator);
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
const confirmed = read.kind === 'staged'
|
|
578
|
-
? read.confirmed
|
|
579
|
-
: {
|
|
580
|
-
record: retyped,
|
|
581
|
-
collection: parseCollection(retyped.documents, compositionOf(retyped)),
|
|
582
|
-
};
|
|
583
|
-
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
|
|
1206
|
+
const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
|
|
1207
|
+
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
584
1208
|
const existing = rooms.get(record.locator);
|
|
585
1209
|
if (existing === undefined)
|
|
586
1210
|
list(record, false);
|
|
@@ -588,25 +1212,17 @@ export function composeRuntime(options) {
|
|
|
588
1212
|
rooms.set(record.locator, {
|
|
589
1213
|
...existing,
|
|
590
1214
|
roomVersion: record.roomVersion,
|
|
591
|
-
attachments: record.attachments,
|
|
592
1215
|
operations: record.operations,
|
|
593
1216
|
replica: record.replica,
|
|
594
|
-
attachedModel: record.attachedModel,
|
|
595
1217
|
model: modelTypes(model),
|
|
596
1218
|
typedSelection: record.typedSelection === null
|
|
597
1219
|
? null
|
|
598
1220
|
: {
|
|
599
1221
|
roots: record.typedSelection.roots,
|
|
600
|
-
attachedModel: record.typedSelection.attachedModel,
|
|
601
1222
|
model: modelTypes(selectionModel),
|
|
602
1223
|
},
|
|
603
|
-
untypedFolders: record.untypedFolders,
|
|
604
1224
|
});
|
|
605
1225
|
}
|
|
606
|
-
if (read.kind === 'refused') {
|
|
607
|
-
refusedSource(record, read);
|
|
608
|
-
declarationRefusals.add(record.locator);
|
|
609
|
-
}
|
|
610
1226
|
}
|
|
611
1227
|
return {
|
|
612
1228
|
lastCompleted,
|
|
@@ -617,16 +1233,26 @@ export function composeRuntime(options) {
|
|
|
617
1233
|
async function readState(opened) {
|
|
618
1234
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
619
1235
|
applyChangeSet(read);
|
|
1236
|
+
prepare();
|
|
620
1237
|
if (replicas.size > 0)
|
|
621
1238
|
emitRooms();
|
|
1239
|
+
if (member !== null)
|
|
1240
|
+
emitMember();
|
|
622
1241
|
for (const locator of locators)
|
|
623
1242
|
emitQueue(locator);
|
|
624
1243
|
publishRoomDrafts(false);
|
|
625
1244
|
return lastCompleted;
|
|
626
1245
|
}
|
|
627
1246
|
async function open() {
|
|
628
|
-
|
|
1247
|
+
if (closed)
|
|
1248
|
+
throw new Error('runtime closed');
|
|
1249
|
+
const opened = await openRuntimeStore(isFenced);
|
|
629
1250
|
store = opened;
|
|
1251
|
+
const interrupted = await opened.fences();
|
|
1252
|
+
for (const locator of interrupted) {
|
|
1253
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1254
|
+
lastFenced.set(locator, ++fencings);
|
|
1255
|
+
}
|
|
630
1256
|
const kept = options.shell.generation;
|
|
631
1257
|
const storedGeneration = await opened.generation();
|
|
632
1258
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -640,6 +1266,8 @@ export function composeRuntime(options) {
|
|
|
640
1266
|
emitStorageLoss();
|
|
641
1267
|
}
|
|
642
1268
|
const lastCompleted = await readState(opened);
|
|
1269
|
+
if (interrupted.length > 0)
|
|
1270
|
+
await erased(opened, interrupted);
|
|
643
1271
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
644
1272
|
return opened;
|
|
645
1273
|
}
|
|
@@ -666,10 +1294,15 @@ export function composeRuntime(options) {
|
|
|
666
1294
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
667
1295
|
}
|
|
668
1296
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
669
|
-
const
|
|
1297
|
+
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
1298
|
+
const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
|
|
670
1299
|
const kept = queue.filter((batch) => !withdrawn.includes(batch));
|
|
1300
|
+
const unqueued = withdrawn.map((batch) => ({
|
|
1301
|
+
batch,
|
|
1302
|
+
returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
|
|
1303
|
+
}));
|
|
671
1304
|
const returning = [
|
|
672
|
-
...
|
|
1305
|
+
...[...unqueued].reverse(),
|
|
673
1306
|
...(settling.leaving === undefined ? [] : [settling.leaving]),
|
|
674
1307
|
];
|
|
675
1308
|
const written = new Map();
|
|
@@ -687,131 +1320,283 @@ export function composeRuntime(options) {
|
|
|
687
1320
|
});
|
|
688
1321
|
}
|
|
689
1322
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
: [{ id: settling.leaving.batch.id, outcome: settling.leaving.outcome }]),
|
|
695
|
-
...withdrawn.map((batch) => ({ id: batch.id, outcome: { kind: 'withdrawn' } })),
|
|
696
|
-
];
|
|
697
|
-
await local.write({
|
|
698
|
-
next: confirmed.record,
|
|
699
|
-
previous: before?.confirmed.record ?? null,
|
|
700
|
-
queue: kept,
|
|
701
|
-
previousQueue: before?.queue ?? [],
|
|
702
|
-
outcomes: settledOutcomes,
|
|
703
|
-
roomDrafts: [...written.values()],
|
|
704
|
-
});
|
|
705
|
-
for (const { id, outcome } of settledOutcomes) {
|
|
706
|
-
outcomes.set(id, outcome);
|
|
707
|
-
outcomeRooms.set(id, locator);
|
|
708
|
-
pendings.delete(id);
|
|
1323
|
+
await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
|
|
1324
|
+
for (const batch of previousQueue) {
|
|
1325
|
+
if (!kept.includes(batch))
|
|
1326
|
+
pendings.delete(batch.id);
|
|
709
1327
|
}
|
|
710
1328
|
for (const record of written.values())
|
|
711
1329
|
rememberRoomDraft(record);
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
1330
|
+
try {
|
|
1331
|
+
await local.write({
|
|
1332
|
+
next: confirmed.record,
|
|
1333
|
+
previous: before?.confirmed.record ?? null,
|
|
1334
|
+
...(settling.stored === undefined ? {} : { stored: settling.stored }),
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
catch (error) {
|
|
1338
|
+
const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
|
|
1339
|
+
if (held !== null)
|
|
1340
|
+
replicas.set(locator, held);
|
|
1341
|
+
else if (kept.length > 0)
|
|
1342
|
+
queuesWithoutReplica.set(locator, kept);
|
|
1343
|
+
else
|
|
1344
|
+
queuesWithoutReplica.delete(locator);
|
|
1345
|
+
publishChangeSet(changeSet(locator, before ?? null, held, {
|
|
1346
|
+
queue: kept.length !== previousQueue.length,
|
|
1347
|
+
drafts: [...written.keys()],
|
|
1348
|
+
}));
|
|
1349
|
+
moveTo({ ...status, ...attention() });
|
|
1350
|
+
throw error;
|
|
1351
|
+
}
|
|
1352
|
+
return () => {
|
|
1353
|
+
queuesWithoutReplica.delete(locator);
|
|
1354
|
+
for (const { batch, returned } of unqueued) {
|
|
1355
|
+
const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
|
|
1356
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1357
|
+
}
|
|
1358
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
1359
|
+
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
1360
|
+
room: settling.room,
|
|
1361
|
+
queue: kept.length !== previousQueue.length,
|
|
1362
|
+
drafts: [...written.keys()],
|
|
1363
|
+
});
|
|
1364
|
+
};
|
|
720
1365
|
}
|
|
721
|
-
function
|
|
1366
|
+
function adoptModels(locator, record) {
|
|
1367
|
+
for (const [attached, model] of [
|
|
1368
|
+
['collection', record.model],
|
|
1369
|
+
['typed-selection', record.selectionModel],
|
|
1370
|
+
]) {
|
|
1371
|
+
const key = modelKey(locator, attached);
|
|
1372
|
+
models.set(key, model);
|
|
1373
|
+
invalidModels.delete(key);
|
|
1374
|
+
if (model.status === 'current')
|
|
1375
|
+
liveModels.set(key, JSON.stringify(model.snapshot));
|
|
1376
|
+
else if (model.status === 'unavailable')
|
|
1377
|
+
liveModels.delete(key);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
function remodel(locator, answers, held) {
|
|
1381
|
+
const taken = (answer, previous, attached) => {
|
|
1382
|
+
if (answer === undefined)
|
|
1383
|
+
return previous;
|
|
1384
|
+
const { state, refused } = attachedModel(answer, previous);
|
|
1385
|
+
if (refused !== null) {
|
|
1386
|
+
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
1387
|
+
journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
|
|
1388
|
+
}
|
|
1389
|
+
models.set(modelKey(locator, attached), state);
|
|
1390
|
+
return state;
|
|
1391
|
+
};
|
|
1392
|
+
return {
|
|
1393
|
+
collection: taken(answers?.collection, held.collection, 'collection'),
|
|
1394
|
+
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
function publish(local, room, confirmed, landing, live) {
|
|
1398
|
+
const { base, stored } = landing;
|
|
722
1399
|
return exclusive(async () => {
|
|
723
|
-
if (
|
|
724
|
-
|
|
1400
|
+
if (stored !== undefined) {
|
|
1401
|
+
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1402
|
+
}
|
|
1403
|
+
if (!live()) {
|
|
1404
|
+
caughtUpTo(room.locator);
|
|
1405
|
+
return true;
|
|
1406
|
+
}
|
|
725
1407
|
const prior = replicas.get(room.locator);
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
const
|
|
734
|
-
const roomVersion = retained.confirmed.record.roomVersion;
|
|
1408
|
+
if (base !== undefined &&
|
|
1409
|
+
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
1410
|
+
prior.confirmed.record.revisions !== base.record.revisions)) {
|
|
1411
|
+
return false;
|
|
1412
|
+
}
|
|
1413
|
+
adoptModels(room.locator, confirmed.record);
|
|
1414
|
+
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
1415
|
+
const roomVersion = confirmed.record.roomVersion;
|
|
735
1416
|
const accepted = queue.filter((batch) => {
|
|
736
|
-
const
|
|
737
|
-
return (
|
|
738
|
-
options.adapter.reached(room.locator, roomVersion, outcome.roomVersion));
|
|
1417
|
+
const accepted = pendings.get(batch.id)?.accepted;
|
|
1418
|
+
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
739
1419
|
});
|
|
740
|
-
|
|
1420
|
+
let settled;
|
|
1421
|
+
try {
|
|
1422
|
+
settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
|
|
1423
|
+
}
|
|
1424
|
+
catch (error) {
|
|
1425
|
+
if (!exceeded(error))
|
|
1426
|
+
throw error;
|
|
1427
|
+
await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
|
|
1428
|
+
caughtUpTo(room.locator);
|
|
1429
|
+
return true;
|
|
1430
|
+
}
|
|
1431
|
+
const set = settled();
|
|
1432
|
+
if (stored !== undefined) {
|
|
1433
|
+
transfers.delete(room.locator);
|
|
1434
|
+
catching.delete(room.locator);
|
|
1435
|
+
}
|
|
741
1436
|
for (const batch of accepted)
|
|
742
1437
|
pendings.delete(batch.id);
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
unreadable.delete(room.locator);
|
|
1438
|
+
stale.delete(room.locator);
|
|
1439
|
+
unindexable.delete(room.locator);
|
|
1440
|
+
if (unreadable.delete(room.locator))
|
|
1441
|
+
scopedRefusals.delete(room.locator);
|
|
748
1442
|
sourceRefusals.delete(room.locator);
|
|
749
1443
|
declarationRefusals.delete(room.locator);
|
|
750
1444
|
list(room, true);
|
|
1445
|
+
if (prior === undefined)
|
|
1446
|
+
prepared.add(room.locator);
|
|
751
1447
|
publishChangeSet(set);
|
|
752
1448
|
moveTo({ ...status, ...attention() });
|
|
1449
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
1450
|
+
return true;
|
|
753
1451
|
});
|
|
754
1452
|
}
|
|
755
1453
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
756
1454
|
const local = await openLocal();
|
|
1455
|
+
await referenced(locator);
|
|
757
1456
|
const replica = replicas.get(locator);
|
|
758
1457
|
const room = rooms.get(locator);
|
|
759
1458
|
if (replica === undefined || room === undefined) {
|
|
760
|
-
throw
|
|
1459
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
761
1460
|
}
|
|
762
1461
|
const saved = [];
|
|
763
1462
|
for (const { path, revision } of named) {
|
|
764
1463
|
const record = roomDrafts.get(locator)?.get(path);
|
|
765
1464
|
if (record === undefined || record.revision !== revision) {
|
|
766
|
-
return { outcome: 'refused',
|
|
1465
|
+
return { outcome: 'refused', code: 'stale', retryable: false, file: path };
|
|
767
1466
|
}
|
|
768
1467
|
if (!saved.some((draft) => draft.path === path))
|
|
769
1468
|
saved.push({ path, parts: record.parts });
|
|
770
1469
|
}
|
|
771
1470
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
772
1471
|
if (changed.length > 0)
|
|
773
|
-
return { outcome: 'refused',
|
|
1472
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
774
1473
|
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
775
|
-
const
|
|
1474
|
+
const changes = parts.flatMap(({ changes }) => changes);
|
|
1475
|
+
const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
1476
|
+
if (requeue.kind === 'refused')
|
|
1477
|
+
return { outcome: 'refused', ...requeue.refusal };
|
|
1478
|
+
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
1479
|
+
const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
|
|
1480
|
+
return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
|
|
1481
|
+
});
|
|
1482
|
+
if (foreign) {
|
|
1483
|
+
return {
|
|
1484
|
+
outcome: 'refused',
|
|
1485
|
+
code: 'unknown-revision',
|
|
1486
|
+
retryable: false,
|
|
1487
|
+
file: [...requeue.queued].at(-1),
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
const withdrawing = requeue.withdrawn.size > 0;
|
|
1491
|
+
const input = {
|
|
1492
|
+
local: localSaves,
|
|
776
1493
|
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
777
1494
|
composition: compositionFor(replica.confirmed.record),
|
|
778
1495
|
operations: room.operations,
|
|
779
1496
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
780
1497
|
providerLimits: limits,
|
|
781
|
-
|
|
1498
|
+
handle: room.handle,
|
|
782
1499
|
visible: replica.visible,
|
|
783
|
-
changes
|
|
1500
|
+
changes,
|
|
784
1501
|
bases: parts.flatMap(({ bases }) => bases),
|
|
785
1502
|
writtenBy,
|
|
1503
|
+
clean: {
|
|
1504
|
+
text: replica.confirmed.record.documents,
|
|
1505
|
+
binaries: replica.confirmed.record.binaries,
|
|
1506
|
+
composition: compositionOf(replica.confirmed.record),
|
|
1507
|
+
},
|
|
1508
|
+
};
|
|
1509
|
+
const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
|
|
1510
|
+
if (checked?.kind === 'refused')
|
|
1511
|
+
return { outcome: 'refused', ...checked.refusal };
|
|
1512
|
+
const owners = [...requeue.owners].flatMap((origin) => {
|
|
1513
|
+
const batch = requeue.withdrawn.get(origin);
|
|
1514
|
+
return batch === undefined || batch === null ? [] : [{ origin, batch }];
|
|
786
1515
|
});
|
|
787
|
-
if (
|
|
1516
|
+
if (owners.length > 1) {
|
|
1517
|
+
return {
|
|
1518
|
+
outcome: 'refused',
|
|
1519
|
+
code: 'unknown-revision',
|
|
1520
|
+
retryable: false,
|
|
1521
|
+
file: [...requeue.queued].at(-1),
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
const built = !withdrawing
|
|
1525
|
+
? buildBatch(input)
|
|
1526
|
+
: requeue.changes.length === 0
|
|
1527
|
+
? null
|
|
1528
|
+
: buildBatch({
|
|
1529
|
+
...input,
|
|
1530
|
+
visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
|
|
1531
|
+
changes: requeue.changes,
|
|
1532
|
+
});
|
|
1533
|
+
if (built?.kind === 'refused')
|
|
788
1534
|
return { outcome: 'refused', ...built.refusal };
|
|
789
|
-
const
|
|
1535
|
+
const [owner] = owners;
|
|
1536
|
+
const join = built === null || owner === undefined
|
|
1537
|
+
? null
|
|
1538
|
+
: joined(requeue.queue, owner.batch, built.batch);
|
|
1539
|
+
if (join?.kind === 'refused')
|
|
1540
|
+
return { outcome: 'refused', ...join.refusal };
|
|
1541
|
+
const withdrawn = new Map(requeue.withdrawn);
|
|
1542
|
+
if (join !== null && owner !== undefined)
|
|
1543
|
+
withdrawn.set(owner.origin, join.batch);
|
|
1544
|
+
const drafted = new Map();
|
|
1545
|
+
for (const [origin, replaced] of withdrawn) {
|
|
1546
|
+
if (replaced === null)
|
|
1547
|
+
continue;
|
|
1548
|
+
const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
|
|
1549
|
+
drafted.set(replaced.id, replaced === join?.batch
|
|
1550
|
+
? [
|
|
1551
|
+
...carried.map((held) => joinedDraft(held, saved)),
|
|
1552
|
+
...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
|
|
1553
|
+
]
|
|
1554
|
+
: carried);
|
|
1555
|
+
}
|
|
1556
|
+
if (built !== null && join === null)
|
|
1557
|
+
drafted.set(built.batch.id, saved);
|
|
1558
|
+
const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
|
|
1559
|
+
for (const replaced of withdrawn.values()) {
|
|
1560
|
+
if (replaced === null || !batches.includes(replaced))
|
|
1561
|
+
continue;
|
|
1562
|
+
const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
1563
|
+
if (limited !== null)
|
|
1564
|
+
return { outcome: 'refused', ...limited };
|
|
1565
|
+
}
|
|
790
1566
|
const consumed = saved.map(({ path }) => path);
|
|
791
1567
|
try {
|
|
792
|
-
await local.
|
|
793
|
-
|
|
794
|
-
previous: replica.confirmed.record,
|
|
1568
|
+
await local.keepAuthored({
|
|
1569
|
+
locator,
|
|
795
1570
|
queue: batches,
|
|
796
|
-
|
|
797
|
-
saved,
|
|
1571
|
+
saved: drafted,
|
|
798
1572
|
consumedRoomDrafts: consumed,
|
|
799
1573
|
});
|
|
800
1574
|
}
|
|
801
1575
|
catch {
|
|
802
|
-
return { outcome: 'refused',
|
|
1576
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
1577
|
+
}
|
|
1578
|
+
if (fences.has(locator))
|
|
1579
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1580
|
+
for (const origin of withdrawn.keys())
|
|
1581
|
+
pendings.delete(origin);
|
|
1582
|
+
for (const [id, drafts] of drafted) {
|
|
1583
|
+
pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
|
|
803
1584
|
}
|
|
804
|
-
|
|
1585
|
+
if (!localSaves)
|
|
1586
|
+
recorded = false;
|
|
805
1587
|
for (const path of consumed)
|
|
806
1588
|
forgetRoomDraft(locator, path);
|
|
807
|
-
replicas.set(locator, replicaOf(replica.confirmed, batches,
|
|
1589
|
+
replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
|
|
808
1590
|
publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
|
|
809
1591
|
queue: true,
|
|
810
1592
|
drafts: consumed,
|
|
811
1593
|
}));
|
|
812
|
-
moveTo({ ...saving(status), ...attention() });
|
|
813
|
-
|
|
814
|
-
|
|
1594
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
1595
|
+
submitLater(locator);
|
|
1596
|
+
const identifier = join?.batch.id ??
|
|
1597
|
+
built?.batch.id ??
|
|
1598
|
+
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
1599
|
+
return { outcome: 'saved', identifier };
|
|
815
1600
|
}
|
|
816
1601
|
async function mark(local, locator, identifier, submission, lost) {
|
|
817
1602
|
const state = pendings.get(identifier);
|
|
@@ -821,6 +1606,8 @@ export function composeRuntime(options) {
|
|
|
821
1606
|
return;
|
|
822
1607
|
}
|
|
823
1608
|
await local.submit(locator, identifier, submission, lost);
|
|
1609
|
+
if (fences.has(locator))
|
|
1610
|
+
return;
|
|
824
1611
|
pendings.set(identifier, { ...state, submission, lost });
|
|
825
1612
|
const replica = replicas.get(locator);
|
|
826
1613
|
if (replica !== undefined)
|
|
@@ -831,22 +1618,24 @@ export function composeRuntime(options) {
|
|
|
831
1618
|
if (!continuing())
|
|
832
1619
|
throw new Error('connection refused');
|
|
833
1620
|
await local.keepUpload(locator, identifier, upload);
|
|
1621
|
+
if (fences.has(locator))
|
|
1622
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
834
1623
|
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
835
1624
|
if (!continuing())
|
|
836
1625
|
throw new Error('connection refused');
|
|
837
1626
|
}
|
|
838
1627
|
async function readHistory(locator, path, query = {}) {
|
|
839
1628
|
if (closed || refusal !== null)
|
|
840
|
-
return
|
|
1629
|
+
return UNCERTAIN;
|
|
841
1630
|
try {
|
|
842
1631
|
await ensureAuthorization();
|
|
843
1632
|
}
|
|
844
1633
|
catch {
|
|
845
|
-
return
|
|
846
|
-
}
|
|
847
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
848
|
-
return { kind: 'transport-uncertainty' };
|
|
1634
|
+
return UNCERTAIN;
|
|
849
1635
|
}
|
|
1636
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1637
|
+
return UNCERTAIN;
|
|
1638
|
+
const since = fencings;
|
|
850
1639
|
const transfer = ['transfer', 'history', locator, path];
|
|
851
1640
|
if (query.revision !== undefined)
|
|
852
1641
|
transfer.push(query.revision);
|
|
@@ -861,6 +1650,8 @@ export function composeRuntime(options) {
|
|
|
861
1650
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
862
1651
|
});
|
|
863
1652
|
journal.record(`outcome history ${result.kind}`);
|
|
1653
|
+
if (fencedSince(locator, since))
|
|
1654
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
864
1655
|
if (result.kind === 'scoped-refusal') {
|
|
865
1656
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
866
1657
|
if (room !== undefined)
|
|
@@ -868,49 +1659,48 @@ export function composeRuntime(options) {
|
|
|
868
1659
|
}
|
|
869
1660
|
if (result.kind === 'revoked-authorization')
|
|
870
1661
|
await refuse('revoked-authorization');
|
|
871
|
-
return result;
|
|
1662
|
+
return historyAnswer(result);
|
|
872
1663
|
}
|
|
873
1664
|
catch {
|
|
874
1665
|
journal.record('outcome history transport-uncertainty');
|
|
875
|
-
return
|
|
1666
|
+
return UNCERTAIN;
|
|
876
1667
|
}
|
|
877
1668
|
}
|
|
878
|
-
function
|
|
879
|
-
for (const
|
|
880
|
-
|
|
881
|
-
|
|
1669
|
+
function queuedBlob(locator, path, localId) {
|
|
1670
|
+
for (const batch of replicas.get(locator)?.queue ?? []) {
|
|
1671
|
+
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
1672
|
+
if (change?.kind === 'binary')
|
|
1673
|
+
return change.blob;
|
|
882
1674
|
}
|
|
883
|
-
return
|
|
1675
|
+
return null;
|
|
884
1676
|
}
|
|
885
|
-
function
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
return undefined;
|
|
1677
|
+
function binaryRoom(request) {
|
|
1678
|
+
if (request.kind === 'path')
|
|
1679
|
+
return request.locator;
|
|
1680
|
+
const scope = options.adapter.referrerScope(request.referrer);
|
|
1681
|
+
return scope?.kind === 'room' ? scope.locator : null;
|
|
893
1682
|
}
|
|
894
|
-
async function
|
|
895
|
-
await ready;
|
|
1683
|
+
async function readBinary(request) {
|
|
896
1684
|
if (closed || refusal !== null)
|
|
897
|
-
return
|
|
898
|
-
if (declaredAction(name) === undefined)
|
|
899
|
-
return { kind: 'undeclared' };
|
|
1685
|
+
return UNCERTAIN;
|
|
900
1686
|
try {
|
|
901
1687
|
await ensureAuthorization();
|
|
902
1688
|
}
|
|
903
1689
|
catch {
|
|
904
|
-
return
|
|
905
|
-
}
|
|
906
|
-
if (closed || authorization === undefined || refusal !== null)
|
|
907
|
-
return
|
|
908
|
-
|
|
909
|
-
journal.record(
|
|
1690
|
+
return UNCERTAIN;
|
|
1691
|
+
}
|
|
1692
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1693
|
+
return UNCERTAIN;
|
|
1694
|
+
const since = fencings;
|
|
1695
|
+
journal.record(request.kind === 'path'
|
|
1696
|
+
? `transfer binary ${request.locator} ${request.path}`
|
|
1697
|
+
: `transfer binary ${request.fileRef}`);
|
|
910
1698
|
try {
|
|
911
|
-
const result = await options.adapter.
|
|
912
|
-
|
|
913
|
-
|
|
1699
|
+
const result = await options.adapter.binary(authorization, request);
|
|
1700
|
+
journal.record(`outcome binary ${result.kind}`);
|
|
1701
|
+
const locator = binaryRoom(request);
|
|
1702
|
+
if (locator !== null && fencedSince(locator, since))
|
|
1703
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
914
1704
|
if (result.kind === 'scoped-refusal') {
|
|
915
1705
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
916
1706
|
if (room !== undefined)
|
|
@@ -918,21 +1708,168 @@ export function composeRuntime(options) {
|
|
|
918
1708
|
}
|
|
919
1709
|
if (result.kind === 'revoked-authorization')
|
|
920
1710
|
await refuse('revoked-authorization');
|
|
921
|
-
|
|
922
|
-
if (result.kind === 'rejected')
|
|
923
|
-
journal.record(`refusal rejected ${result.code}`);
|
|
924
|
-
return result;
|
|
1711
|
+
return binaryAnswer(result);
|
|
925
1712
|
}
|
|
926
1713
|
catch {
|
|
927
|
-
journal.record('outcome
|
|
928
|
-
return
|
|
1714
|
+
journal.record('outcome binary transport-uncertainty');
|
|
1715
|
+
return UNCERTAIN;
|
|
929
1716
|
}
|
|
930
1717
|
}
|
|
931
|
-
function
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
const
|
|
1718
|
+
async function readBinaryPath(locator, path, revision) {
|
|
1719
|
+
await ready;
|
|
1720
|
+
if (revision !== undefined)
|
|
1721
|
+
return readBinary({ kind: 'path', locator, path, revision });
|
|
1722
|
+
const visible = replicas.get(locator)?.visible;
|
|
1723
|
+
const held = visible?.binaries[path];
|
|
1724
|
+
if (visible === undefined || held === undefined)
|
|
1725
|
+
return absentBinary(path);
|
|
1726
|
+
if (held.state === 'pending') {
|
|
1727
|
+
const blob = queuedBlob(locator, path, held.localId);
|
|
1728
|
+
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1729
|
+
}
|
|
1730
|
+
const record = binaryRecord(path, held, visible.revisions[path]);
|
|
1731
|
+
if (record.state !== 'accepted')
|
|
1732
|
+
return absentBinary(path);
|
|
1733
|
+
const replica = replicas.get(locator);
|
|
1734
|
+
const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
|
|
1735
|
+
const holds = (at) => {
|
|
1736
|
+
const kept = replica?.confirmed.record.binaries[at];
|
|
1737
|
+
return (kept?.state === 'accepted' &&
|
|
1738
|
+
kept.fileRef === record.fileRef &&
|
|
1739
|
+
replica?.confirmed.record.revisions[at] === record.revision);
|
|
1740
|
+
};
|
|
1741
|
+
let confirmed = path;
|
|
1742
|
+
for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
|
|
1743
|
+
const change = queued[index];
|
|
1744
|
+
if (change.kind === 'move' && change.to === confirmed)
|
|
1745
|
+
confirmed = change.path;
|
|
1746
|
+
}
|
|
1747
|
+
return readBinary({
|
|
1748
|
+
kind: 'path',
|
|
1749
|
+
locator,
|
|
1750
|
+
path: confirmed,
|
|
1751
|
+
revision: record.revision,
|
|
1752
|
+
fileRef: record.fileRef,
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
async function readBinaryReference(reference) {
|
|
1756
|
+
await ready;
|
|
1757
|
+
return readBinary({
|
|
1758
|
+
kind: 'reference',
|
|
1759
|
+
fileRef: reference.fileRef,
|
|
1760
|
+
referrer: reference.referrer,
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
function declaredAgents() {
|
|
1764
|
+
for (const capability of capabilities) {
|
|
1765
|
+
if (capability.conversationAgents !== undefined)
|
|
1766
|
+
return capability.conversationAgents;
|
|
1767
|
+
}
|
|
1768
|
+
return undefined;
|
|
1769
|
+
}
|
|
1770
|
+
function declaredAction(name) {
|
|
1771
|
+
for (const capability of capabilities) {
|
|
1772
|
+
for (const action of capability.actions ?? []) {
|
|
1773
|
+
if (action.name === name)
|
|
1774
|
+
return action;
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return undefined;
|
|
1778
|
+
}
|
|
1779
|
+
async function invokeAction(name, input, dispatching) {
|
|
1780
|
+
await ready;
|
|
1781
|
+
if (closed || refusal !== null)
|
|
1782
|
+
return UNCERTAIN;
|
|
1783
|
+
if (declaredAction(name) === undefined)
|
|
1784
|
+
return undeclared(name);
|
|
1785
|
+
try {
|
|
1786
|
+
await ensureAuthorization();
|
|
1787
|
+
}
|
|
1788
|
+
catch {
|
|
1789
|
+
return UNCERTAIN;
|
|
1790
|
+
}
|
|
1791
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1792
|
+
return UNCERTAIN;
|
|
1793
|
+
await dispatching?.();
|
|
1794
|
+
journal.record(`transfer action ${name}`);
|
|
1795
|
+
try {
|
|
1796
|
+
const result = await options.adapter.action(authorization, { name, input });
|
|
1797
|
+
if (result.kind === 'scoped-refusal') {
|
|
1798
|
+
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1799
|
+
if (room !== undefined)
|
|
1800
|
+
await scoped(result, room, await openLocal());
|
|
1801
|
+
}
|
|
1802
|
+
if (result.kind === 'revoked-authorization')
|
|
1803
|
+
await refuse('revoked-authorization');
|
|
1804
|
+
journal.record(`outcome action ${result.kind}`);
|
|
1805
|
+
if (result.kind === 'rejected')
|
|
1806
|
+
journal.record(`refusal rejected ${result.code}`);
|
|
1807
|
+
return calledResult(result);
|
|
1808
|
+
}
|
|
1809
|
+
catch {
|
|
1810
|
+
journal.record('outcome action transport-uncertainty');
|
|
1811
|
+
return UNCERTAIN;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
function unsettled(result) {
|
|
1815
|
+
return result.kind !== 'action' && result.retryable;
|
|
1816
|
+
}
|
|
1817
|
+
async function recoveredAction(name, input) {
|
|
1818
|
+
const operation = operationOf(input);
|
|
1819
|
+
if (operation === null)
|
|
1820
|
+
return invokeAction(name, input);
|
|
1821
|
+
const kept = async () => (await openLocal()).keepCall({ operation, name, input });
|
|
1822
|
+
calling.add(operation);
|
|
1823
|
+
try {
|
|
1824
|
+
let result = await invokeAction(name, input, kept);
|
|
1825
|
+
if (unsettled(result)) {
|
|
1826
|
+
await recovery.attempt(`action ${operation}`, async () => {
|
|
1827
|
+
if (closed || refusal !== null)
|
|
1828
|
+
return 'done';
|
|
1829
|
+
if (!socketOpen)
|
|
1830
|
+
return 'again';
|
|
1831
|
+
result = await invokeAction(name, input, kept);
|
|
1832
|
+
return unsettled(result) ? 'again' : 'done';
|
|
1833
|
+
}, { again: true });
|
|
1834
|
+
}
|
|
1835
|
+
if (!unsettled(result))
|
|
1836
|
+
await (await openLocal()).dropCall(operation);
|
|
1837
|
+
return result;
|
|
1838
|
+
}
|
|
1839
|
+
finally {
|
|
1840
|
+
calling.delete(operation);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
async function resendCalls(signal) {
|
|
1844
|
+
if (!socketOpen || closed || stopping)
|
|
1845
|
+
return 'done';
|
|
1846
|
+
const local = await openLocal();
|
|
1847
|
+
for (const held of await local.calls()) {
|
|
1848
|
+
if (calling.has(held.operation))
|
|
1849
|
+
continue;
|
|
1850
|
+
if (closed || signal.aborted)
|
|
1851
|
+
return 'done';
|
|
1852
|
+
if (pastHorizon(held.operation)) {
|
|
1853
|
+
await local.dropCall(held.operation);
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
calling.add(held.operation);
|
|
1857
|
+
try {
|
|
1858
|
+
if (unsettled(await invokeAction(held.name, held.input)))
|
|
1859
|
+
return 'again';
|
|
1860
|
+
await local.dropCall(held.operation);
|
|
1861
|
+
}
|
|
1862
|
+
finally {
|
|
1863
|
+
calling.delete(held.operation);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
return 'done';
|
|
1867
|
+
}
|
|
1868
|
+
function continuing() {
|
|
1869
|
+
return !closed && !discoveryPaused;
|
|
1870
|
+
}
|
|
1871
|
+
function reachedBy(locator, roomVersion) {
|
|
1872
|
+
const held = replicas.get(locator);
|
|
936
1873
|
return (held !== undefined &&
|
|
937
1874
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion));
|
|
938
1875
|
}
|
|
@@ -942,32 +1879,12 @@ export function composeRuntime(options) {
|
|
|
942
1879
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
943
1880
|
!options.adapter.reached(locator, roomVersion, written));
|
|
944
1881
|
}
|
|
945
|
-
function
|
|
946
|
-
if (
|
|
947
|
-
return;
|
|
948
|
-
const held = wanted.get(locator);
|
|
949
|
-
if (held === undefined || options.adapter.reached(locator, roomVersion, held)) {
|
|
950
|
-
wanted.set(locator, roomVersion);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
function reachOf(locator) {
|
|
954
|
-
let target = wanted.get(locator);
|
|
955
|
-
const versions = (replicas.get(locator)?.queue ?? []).flatMap((batch) => {
|
|
956
|
-
const outcome = outcomes.get(batch.id);
|
|
957
|
-
return outcome?.kind === 'accepted' ? [outcome.roomVersion] : [];
|
|
958
|
-
});
|
|
959
|
-
for (const version of versions) {
|
|
960
|
-
if (target === undefined || options.adapter.reached(locator, version, target)) {
|
|
961
|
-
target = version;
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
return target === undefined || reachedBy(locator, target) ? undefined : target;
|
|
965
|
-
}
|
|
966
|
-
async function pullRefresh(authorized, confirmed, minPosition) {
|
|
967
|
-
if (!continuing())
|
|
1882
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
1883
|
+
if (!live())
|
|
968
1884
|
return unconfirmed;
|
|
1885
|
+
const turn = generation;
|
|
969
1886
|
try {
|
|
970
|
-
const result = await refreshRoom(
|
|
1887
|
+
const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
971
1888
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
972
1889
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
973
1890
|
return result;
|
|
@@ -976,56 +1893,153 @@ export function composeRuntime(options) {
|
|
|
976
1893
|
journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
|
|
977
1894
|
throw error;
|
|
978
1895
|
}
|
|
1896
|
+
finally {
|
|
1897
|
+
reads.finished(confirmed.record.locator, turn);
|
|
1898
|
+
}
|
|
979
1899
|
}
|
|
980
|
-
|
|
981
|
-
|
|
1900
|
+
function staging(local, live) {
|
|
1901
|
+
return {
|
|
1902
|
+
held: (locator) => transfers.get(locator) ?? null,
|
|
1903
|
+
staged: (record) => local.staged(record),
|
|
1904
|
+
stage: (record, page) => exclusive(async () => {
|
|
1905
|
+
if (!live())
|
|
1906
|
+
return false;
|
|
1907
|
+
const bytes = layerBytes(page);
|
|
1908
|
+
if (!(await admitted(local, record.locator, bytes)))
|
|
1909
|
+
return false;
|
|
1910
|
+
try {
|
|
1911
|
+
await local.stage(record, page);
|
|
1912
|
+
}
|
|
1913
|
+
catch (error) {
|
|
1914
|
+
if (!exceeded(error))
|
|
1915
|
+
throw error;
|
|
1916
|
+
await exceededOn(record.locator, bytes);
|
|
1917
|
+
return false;
|
|
1918
|
+
}
|
|
1919
|
+
transfers.set(record.locator, record);
|
|
1920
|
+
readSide.recover();
|
|
1921
|
+
return true;
|
|
1922
|
+
}),
|
|
1923
|
+
abandon: (locator) => exclusive(async () => {
|
|
1924
|
+
if (!live())
|
|
1925
|
+
return;
|
|
1926
|
+
await local.abandon(locator);
|
|
1927
|
+
transfers.delete(locator);
|
|
1928
|
+
readSide.recover();
|
|
1929
|
+
}),
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
function reusable(room) {
|
|
1933
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1934
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1935
|
+
return null;
|
|
1936
|
+
return { documents: record.documents, revisions: record.revisions };
|
|
1937
|
+
}
|
|
1938
|
+
async function pullBootstrap(authorized, room, local, live) {
|
|
1939
|
+
if (!live())
|
|
982
1940
|
return unconfirmed;
|
|
1941
|
+
const turn = generation;
|
|
1942
|
+
if (replicas.has(room.locator)) {
|
|
1943
|
+
catching.add(room.locator);
|
|
1944
|
+
readSide.recover();
|
|
1945
|
+
}
|
|
983
1946
|
try {
|
|
984
|
-
const result = await bootstrapRoom(
|
|
1947
|
+
const result = await bootstrapRoom(reads, authorized, room, {
|
|
985
1948
|
collection: modelOf(room.locator),
|
|
986
1949
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
987
|
-
},
|
|
1950
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
988
1951
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
989
1952
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1953
|
+
if (result.kind !== 'staged')
|
|
1954
|
+
caughtUpTo(room.locator);
|
|
990
1955
|
return result;
|
|
991
1956
|
}
|
|
992
1957
|
catch (error) {
|
|
993
1958
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1959
|
+
caughtUpTo(room.locator);
|
|
994
1960
|
throw error;
|
|
995
1961
|
}
|
|
1962
|
+
finally {
|
|
1963
|
+
reads.finished(room.locator, turn);
|
|
1964
|
+
}
|
|
996
1965
|
}
|
|
997
|
-
|
|
1966
|
+
function caughtUpTo(locator) {
|
|
1967
|
+
if (catching.delete(locator))
|
|
1968
|
+
readSide.recover();
|
|
1969
|
+
}
|
|
1970
|
+
function behind(room) {
|
|
1971
|
+
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1972
|
+
return (held !== undefined &&
|
|
1973
|
+
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
1974
|
+
}
|
|
1975
|
+
async function advance(room, local, authorized, live) {
|
|
1976
|
+
const carried = reads.carried(room.locator);
|
|
1977
|
+
const withheld = await advanceOnce(room, local, authorized, live);
|
|
1978
|
+
if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
|
|
1979
|
+
return withheld;
|
|
1980
|
+
}
|
|
1981
|
+
return advanceOnce(room, local, authorized, live);
|
|
1982
|
+
}
|
|
1983
|
+
async function advanceOnce(room, local, authorized, live) {
|
|
1984
|
+
await referenced(room.locator);
|
|
1985
|
+
if (!live() || paused.has(room.locator))
|
|
1986
|
+
return unconfirmed;
|
|
998
1987
|
const held = replicas.get(room.locator);
|
|
999
1988
|
if (held !== undefined) {
|
|
1000
|
-
const refreshed = await pullRefresh(authorized, held.confirmed,
|
|
1001
|
-
if (!
|
|
1989
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
1990
|
+
if (!live())
|
|
1002
1991
|
return unconfirmed;
|
|
1003
1992
|
if (refreshed.kind === 'still-current') {
|
|
1993
|
+
stale.delete(room.locator);
|
|
1004
1994
|
if (sourceRefusals.has(room.locator))
|
|
1005
|
-
return
|
|
1995
|
+
return null;
|
|
1006
1996
|
if (unreadable.delete(room.locator)) {
|
|
1997
|
+
scopedRefusals.delete(room.locator);
|
|
1007
1998
|
list(room, true);
|
|
1008
1999
|
emitRooms();
|
|
1009
2000
|
}
|
|
1010
2001
|
return null;
|
|
1011
2002
|
}
|
|
1012
2003
|
if (refreshed.kind === 'advanced') {
|
|
1013
|
-
await publish(local, room, refreshed.confirmed)
|
|
2004
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
2005
|
+
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
2006
|
+
const reached = now !== undefined &&
|
|
2007
|
+
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
2008
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
2009
|
+
}
|
|
2010
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1014
2011
|
return null;
|
|
1015
2012
|
}
|
|
1016
2013
|
if (refreshed.kind !== 'beyond-horizon')
|
|
1017
2014
|
return refreshed;
|
|
2015
|
+
if (opened !== null) {
|
|
2016
|
+
owed.add(room.locator);
|
|
2017
|
+
readSide.recover();
|
|
2018
|
+
return unconfirmed;
|
|
2019
|
+
}
|
|
1018
2020
|
}
|
|
1019
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
1020
|
-
if (!
|
|
2021
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
2022
|
+
if (!live())
|
|
1021
2023
|
return unconfirmed;
|
|
1022
2024
|
if (bootstrapped.kind !== 'staged')
|
|
1023
2025
|
return bootstrapped;
|
|
1024
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
2026
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1025
2027
|
return null;
|
|
1026
2028
|
}
|
|
1027
2029
|
async function withhold(withheld, room, local, progress) {
|
|
1028
|
-
|
|
2030
|
+
if (withheld.kind === 'unconfirmed')
|
|
2031
|
+
return;
|
|
2032
|
+
if (withheld.kind === 'refused')
|
|
2033
|
+
stale.delete(room.locator);
|
|
2034
|
+
else
|
|
2035
|
+
stale.add(room.locator);
|
|
2036
|
+
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
2037
|
+
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
2038
|
+
repairable.set(room.locator, reset);
|
|
2039
|
+
repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
|
|
2040
|
+
}
|
|
2041
|
+
else
|
|
2042
|
+
repairable.delete(room.locator);
|
|
1029
2043
|
if (withheld.kind === 'scoped-refusal') {
|
|
1030
2044
|
await scoped(withheld, room, local);
|
|
1031
2045
|
}
|
|
@@ -1050,6 +2064,7 @@ export function composeRuntime(options) {
|
|
|
1050
2064
|
function refusedSource(room, { refusal }) {
|
|
1051
2065
|
sourceRefusals.set(room.locator, {
|
|
1052
2066
|
code: refusal.rule,
|
|
2067
|
+
retryable: false,
|
|
1053
2068
|
fields: {
|
|
1054
2069
|
file: refusal.file,
|
|
1055
2070
|
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
@@ -1060,37 +2075,20 @@ export function composeRuntime(options) {
|
|
|
1060
2075
|
unreadable.add(room.locator);
|
|
1061
2076
|
list(room, false);
|
|
1062
2077
|
emitRooms();
|
|
2078
|
+
moveTo({ ...status, ...attention() });
|
|
1063
2079
|
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1064
2080
|
}
|
|
1065
2081
|
function leaving(batch, result) {
|
|
1066
|
-
if (result.kind === 'expired')
|
|
1067
|
-
return { batch,
|
|
1068
|
-
}
|
|
1069
|
-
const refusal = { code: result.code, fields: result.fields };
|
|
1070
|
-
if (result.kind === 'conflicted') {
|
|
1071
|
-
return {
|
|
1072
|
-
batch,
|
|
1073
|
-
outcome: {
|
|
1074
|
-
kind: 'conflicted',
|
|
1075
|
-
...(result.roomVersion === undefined ? {} : { roomVersion: result.roomVersion }),
|
|
1076
|
-
affected: result.affected,
|
|
1077
|
-
...refusal,
|
|
1078
|
-
},
|
|
1079
|
-
returned: { kind: 'conflicted', ...refusal },
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
2082
|
+
if (result.kind === 'expired')
|
|
2083
|
+
return { batch, returned: returnedBy('expired') };
|
|
1082
2084
|
return {
|
|
1083
2085
|
batch,
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
kind: 'accepted',
|
|
1091
|
-
roomVersion: accepted.roomVersion,
|
|
1092
|
-
...(accepted.author === undefined ? {} : { author: accepted.author }),
|
|
1093
|
-
...(accepted.time === undefined ? {} : { time: accepted.time }),
|
|
2086
|
+
returned: {
|
|
2087
|
+
kind: result.kind,
|
|
2088
|
+
code: result.code,
|
|
2089
|
+
fields: result.fields,
|
|
2090
|
+
retryable: result.retryable,
|
|
2091
|
+
},
|
|
1094
2092
|
};
|
|
1095
2093
|
}
|
|
1096
2094
|
function logAccepted(locator, submitted, accepted) {
|
|
@@ -1098,84 +2096,56 @@ export function composeRuntime(options) {
|
|
|
1098
2096
|
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
1099
2097
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1100
2098
|
}
|
|
1101
|
-
async function
|
|
1102
|
-
await
|
|
1103
|
-
if (!continuing())
|
|
1104
|
-
return;
|
|
1105
|
-
const current = replicas.get(room.locator);
|
|
1106
|
-
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1107
|
-
return;
|
|
1108
|
-
}
|
|
1109
|
-
const outcome = acceptedOf(accepted);
|
|
1110
|
-
await local.write({
|
|
1111
|
-
next: current.confirmed.record,
|
|
1112
|
-
previous: current.confirmed.record,
|
|
1113
|
-
queue: current.queue,
|
|
1114
|
-
previousQueue: current.queue,
|
|
1115
|
-
outcomes: [{ id: submitted.id, outcome }],
|
|
1116
|
-
});
|
|
1117
|
-
outcomes.set(submitted.id, outcome);
|
|
1118
|
-
outcomeRooms.set(submitted.id, room.locator);
|
|
1119
|
-
logAccepted(room.locator, submitted, accepted);
|
|
1120
|
-
});
|
|
1121
|
-
}
|
|
1122
|
-
async function commitAccept(room, local, submitted, accepted, base, overlay) {
|
|
2099
|
+
async function adoptAccepted(room, local, submitted, accepted) {
|
|
2100
|
+
await referenced(room.locator);
|
|
1123
2101
|
if (!continuing())
|
|
1124
2102
|
return unconfirmed;
|
|
1125
2103
|
const current = replicas.get(room.locator);
|
|
1126
2104
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1127
2105
|
return null;
|
|
1128
2106
|
}
|
|
1129
|
-
const
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
2107
|
+
const held = current.confirmed.record.roomVersion;
|
|
2108
|
+
logAccepted(room.locator, submitted, accepted);
|
|
2109
|
+
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
2110
|
+
? { kind: 'staged', confirmed: current.confirmed }
|
|
2111
|
+
: applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
2112
|
+
? accepted.roomVersion
|
|
2113
|
+
: held, accepted.roomVersion);
|
|
2114
|
+
if (applied.kind !== 'staged') {
|
|
2115
|
+
const state = pendings.get(submitted.id);
|
|
2116
|
+
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
2117
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
2118
|
+
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
2119
|
+
emitQueue(room.locator);
|
|
1139
2120
|
return applied;
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
});
|
|
2121
|
+
}
|
|
2122
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
2123
|
+
pendings.delete(submitted.id);
|
|
1144
2124
|
unreadable.delete(room.locator);
|
|
1145
2125
|
sourceRefusals.delete(room.locator);
|
|
1146
2126
|
declarationRefusals.delete(room.locator);
|
|
2127
|
+
const set = settled();
|
|
1147
2128
|
list(room, true);
|
|
1148
2129
|
publishChangeSet(set);
|
|
1149
2130
|
moveTo({ ...status, ...attention() });
|
|
1150
2131
|
return null;
|
|
1151
2132
|
}
|
|
1152
|
-
async function
|
|
1153
|
-
const
|
|
1154
|
-
if (options.adapter.reached(room.locator, roomVersion, accepted.roomVersion)) {
|
|
1155
|
-
return committed(room, local, submitted, accepted, pulled, roomVersion === accepted.roomVersion);
|
|
1156
|
-
}
|
|
1157
|
-
if (fresh && continuing())
|
|
1158
|
-
await publish(local, room, pulled);
|
|
1159
|
-
return false;
|
|
1160
|
-
}
|
|
1161
|
-
async function committed(room, local, submitted, accepted, base, overlay) {
|
|
1162
|
-
const failed = await exclusive(() => commitAccept(room, local, submitted, accepted, base, overlay));
|
|
2133
|
+
async function adopted(room, local, submitted, accepted) {
|
|
2134
|
+
const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
|
|
1163
2135
|
if (failed === null)
|
|
1164
2136
|
return true;
|
|
1165
2137
|
if (failed.kind === 'refused' && continuing())
|
|
1166
2138
|
refusedSource(room, failed);
|
|
1167
2139
|
return false;
|
|
1168
2140
|
}
|
|
1169
|
-
async function submitRoom(room, local, authorized, progress) {
|
|
1170
|
-
while (continuing() && authorization !== undefined) {
|
|
2141
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
2142
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1171
2143
|
const replica = replicas.get(room.locator);
|
|
1172
2144
|
const head = replica?.queue[0];
|
|
1173
2145
|
if (replica === undefined || head === undefined)
|
|
1174
|
-
return;
|
|
1175
|
-
if (
|
|
1176
|
-
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
2146
|
+
return 'done';
|
|
2147
|
+
if (pendings.get(head.id)?.accepted !== undefined)
|
|
2148
|
+
return 'done';
|
|
1179
2149
|
const state = pendings.get(head.id);
|
|
1180
2150
|
const { submission, lost: earlier } = state ?? {
|
|
1181
2151
|
submission: { state: 'waiting' },
|
|
@@ -1183,26 +2153,33 @@ export function composeRuntime(options) {
|
|
|
1183
2153
|
};
|
|
1184
2154
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1185
2155
|
progress.complete = false;
|
|
1186
|
-
return;
|
|
2156
|
+
return { at: Date.parse(submission.reset) };
|
|
1187
2157
|
}
|
|
1188
2158
|
if (writesHeld()) {
|
|
1189
2159
|
progress.complete = false;
|
|
1190
2160
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1191
|
-
return;
|
|
2161
|
+
return 'done';
|
|
1192
2162
|
}
|
|
1193
2163
|
const lost = earlier || submission.state === 'submitted';
|
|
2164
|
+
if (lost && pastHorizon(head.id)) {
|
|
2165
|
+
await discardPending(room, local, signal);
|
|
2166
|
+
return 'done';
|
|
2167
|
+
}
|
|
1194
2168
|
let result;
|
|
1195
2169
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1196
2170
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1197
|
-
if (discoveryPaused)
|
|
1198
|
-
return;
|
|
1199
|
-
if (writesHeld()) {
|
|
2171
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
2172
|
+
return 'done';
|
|
2173
|
+
if (signal.aborted || writesHeld()) {
|
|
1200
2174
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
2175
|
+
if (signal.aborted)
|
|
2176
|
+
return 'done';
|
|
1201
2177
|
progress.complete = false;
|
|
1202
2178
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1203
|
-
return;
|
|
2179
|
+
return 'done';
|
|
1204
2180
|
}
|
|
1205
2181
|
const opensBefore = openSignals;
|
|
2182
|
+
const answered = awaitingAnswer(room.locator);
|
|
1206
2183
|
try {
|
|
1207
2184
|
result = await options.adapter.mutate(authorized, {
|
|
1208
2185
|
locator: room.locator,
|
|
@@ -1211,33 +2188,40 @@ export function composeRuntime(options) {
|
|
|
1211
2188
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
1212
2189
|
}
|
|
1213
2190
|
catch {
|
|
2191
|
+
answered();
|
|
1214
2192
|
progress.complete = false;
|
|
1215
2193
|
if (continuing())
|
|
1216
2194
|
progress.reachable = false;
|
|
1217
|
-
return;
|
|
2195
|
+
return 'again';
|
|
2196
|
+
}
|
|
2197
|
+
if (signal.aborted) {
|
|
2198
|
+
answered();
|
|
2199
|
+
return 'done';
|
|
1218
2200
|
}
|
|
2201
|
+
if (result.kind !== 'accepted' || !continuing())
|
|
2202
|
+
answered();
|
|
1219
2203
|
if (!continuing())
|
|
1220
|
-
return;
|
|
2204
|
+
return 'done';
|
|
1221
2205
|
if (result.kind === 'transport-uncertainty') {
|
|
1222
2206
|
progress.complete = false;
|
|
1223
2207
|
progress.reachable = false;
|
|
1224
|
-
return;
|
|
2208
|
+
return 'again';
|
|
1225
2209
|
}
|
|
1226
2210
|
if (result.kind === 'room-not-readable') {
|
|
1227
2211
|
progress.complete = false;
|
|
1228
2212
|
unreadable.add(room.locator);
|
|
1229
2213
|
list(room, false);
|
|
1230
2214
|
emitRooms();
|
|
1231
|
-
return;
|
|
2215
|
+
return 'done';
|
|
1232
2216
|
}
|
|
1233
2217
|
if (result.kind === 'revoked-authorization') {
|
|
1234
2218
|
await refuse('revoked-authorization');
|
|
1235
|
-
return;
|
|
2219
|
+
return 'done';
|
|
1236
2220
|
}
|
|
1237
2221
|
if (result.kind === 'scoped-refusal') {
|
|
1238
2222
|
await scoped(result, room, local);
|
|
1239
2223
|
progress.complete = false;
|
|
1240
|
-
return;
|
|
2224
|
+
return 'done';
|
|
1241
2225
|
}
|
|
1242
2226
|
if (result.kind === 'write-halted') {
|
|
1243
2227
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
@@ -1246,16 +2230,24 @@ export function composeRuntime(options) {
|
|
|
1246
2230
|
continue;
|
|
1247
2231
|
haltedRefusal = true;
|
|
1248
2232
|
progress.complete = false;
|
|
1249
|
-
return;
|
|
2233
|
+
return 'done';
|
|
1250
2234
|
}
|
|
1251
2235
|
if (result.kind === 'conflicted' ||
|
|
1252
2236
|
result.kind === 'rejected' ||
|
|
1253
2237
|
result.kind === 'expired' ||
|
|
1254
2238
|
result.kind === 'retryable-refusal') {
|
|
2239
|
+
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2240
|
+
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2241
|
+
await discardPending(room, local, signal);
|
|
2242
|
+
return 'done';
|
|
2243
|
+
}
|
|
1255
2244
|
if (result.kind === 'expired') {
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
2245
|
+
await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
|
|
2246
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2247
|
+
if (withheld !== null) {
|
|
2248
|
+
await withhold(withheld, room, local, progress);
|
|
2249
|
+
return 'done';
|
|
2250
|
+
}
|
|
1259
2251
|
}
|
|
1260
2252
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1261
2253
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1268,7 +2260,7 @@ export function composeRuntime(options) {
|
|
|
1268
2260
|
const reset = result.reset;
|
|
1269
2261
|
progress.complete = false;
|
|
1270
2262
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1271
|
-
return;
|
|
2263
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1272
2264
|
}
|
|
1273
2265
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1274
2266
|
const left = leaving(head, result);
|
|
@@ -1279,113 +2271,96 @@ export function composeRuntime(options) {
|
|
|
1279
2271
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1280
2272
|
return;
|
|
1281
2273
|
}
|
|
1282
|
-
const
|
|
1283
|
-
leaving: result.kind === 'expired' && landed(head, current.confirmed)
|
|
2274
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
2275
|
+
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
2276
|
+
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1284
2277
|
? { ...left, returned: null }
|
|
1285
2278
|
: left,
|
|
1286
2279
|
});
|
|
1287
|
-
publishChangeSet(
|
|
2280
|
+
publishChangeSet(settled());
|
|
1288
2281
|
moveTo({ ...status, ...attention() });
|
|
1289
2282
|
});
|
|
1290
2283
|
if (result.kind === 'expired')
|
|
1291
2284
|
continue;
|
|
1292
|
-
const withheld = await advance(room, local, authorized);
|
|
1293
|
-
if (withheld !== null)
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
}
|
|
1297
|
-
await recordAccepted(room, local, head, result);
|
|
1298
|
-
const from = replicas.get(room.locator)?.confirmed;
|
|
1299
|
-
if (from === undefined)
|
|
1300
|
-
return;
|
|
1301
|
-
const pulled = await pullRefresh(authorized, from, reachOf(room.locator));
|
|
1302
|
-
if (!continuing())
|
|
1303
|
-
return;
|
|
1304
|
-
if (pulled.kind === 'revoked-authorization') {
|
|
1305
|
-
await refuse('revoked-authorization');
|
|
1306
|
-
return;
|
|
1307
|
-
}
|
|
1308
|
-
if (pulled.kind === 'scoped-refusal') {
|
|
1309
|
-
await scoped(pulled, room, local);
|
|
1310
|
-
progress.complete = false;
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
if (pulled.kind === 'still-current' || pulled.kind === 'advanced') {
|
|
1314
|
-
const fresh = pulled.kind === 'advanced';
|
|
1315
|
-
if (!(await accepting(room, local, head, result, fresh ? pulled.confirmed : from, fresh))) {
|
|
1316
|
-
progress.complete = false;
|
|
1317
|
-
return;
|
|
2285
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2286
|
+
if (withheld !== null) {
|
|
2287
|
+
await withhold(withheld, room, local, progress);
|
|
2288
|
+
return 'done';
|
|
1318
2289
|
}
|
|
1319
2290
|
continue;
|
|
1320
2291
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
if (!continuing())
|
|
1324
|
-
return;
|
|
1325
|
-
if (bootstrapped.kind === 'revoked-authorization') {
|
|
1326
|
-
await refuse('revoked-authorization');
|
|
1327
|
-
return;
|
|
1328
|
-
}
|
|
1329
|
-
if (bootstrapped.kind === 'scoped-refusal') {
|
|
1330
|
-
await scoped(bootstrapped, room, local);
|
|
1331
|
-
progress.complete = false;
|
|
1332
|
-
return;
|
|
1333
|
-
}
|
|
1334
|
-
if (bootstrapped.kind === 'staged') {
|
|
1335
|
-
if (!(await accepting(room, local, head, result, bootstrapped.confirmed, true))) {
|
|
1336
|
-
progress.complete = false;
|
|
1337
|
-
return;
|
|
1338
|
-
}
|
|
1339
|
-
continue;
|
|
1340
|
-
}
|
|
2292
|
+
const kept = await adopted(room, local, head, result).finally(answered);
|
|
2293
|
+
if (!kept) {
|
|
1341
2294
|
progress.complete = false;
|
|
1342
|
-
|
|
1343
|
-
progress.reachable = false;
|
|
1344
|
-
}
|
|
1345
|
-
else if (bootstrapped.kind === 'room-not-readable') {
|
|
1346
|
-
unreadable.add(room.locator);
|
|
1347
|
-
list(room, false);
|
|
1348
|
-
emitRooms();
|
|
1349
|
-
}
|
|
1350
|
-
else if (bootstrapped.kind === 'refused') {
|
|
1351
|
-
refusedSource(room, bootstrapped);
|
|
1352
|
-
}
|
|
1353
|
-
return;
|
|
2295
|
+
return 'done';
|
|
1354
2296
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
2297
|
+
}
|
|
2298
|
+
return 'done';
|
|
2299
|
+
}
|
|
2300
|
+
function pastHorizon(identity) {
|
|
2301
|
+
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2302
|
+
}
|
|
2303
|
+
async function discardPending(room, local, signal) {
|
|
2304
|
+
const { locator } = room;
|
|
2305
|
+
const discarded = await exclusive(async () => {
|
|
2306
|
+
if (fences.has(locator))
|
|
2307
|
+
return false;
|
|
2308
|
+
const before = replicas.get(locator);
|
|
2309
|
+
const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
2310
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
2311
|
+
lastFenced.set(locator, ++fencings);
|
|
2312
|
+
try {
|
|
2313
|
+
await local.discard(locator);
|
|
1366
2314
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
2315
|
+
finally {
|
|
2316
|
+
fences.delete(locator);
|
|
1369
2317
|
}
|
|
2318
|
+
for (const batch of queue)
|
|
2319
|
+
pendings.delete(batch.id);
|
|
2320
|
+
queuesWithoutReplica.delete(locator);
|
|
2321
|
+
const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
|
|
2322
|
+
if (after !== null)
|
|
2323
|
+
replicas.set(locator, after);
|
|
2324
|
+
journal.record(`room discarded ${locator}`);
|
|
2325
|
+
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
2326
|
+
moveTo({ ...status, ...attention() });
|
|
2327
|
+
return true;
|
|
2328
|
+
});
|
|
2329
|
+
if (!discarded)
|
|
1370
2330
|
return;
|
|
1371
|
-
}
|
|
1372
|
-
|
|
2331
|
+
const progress = { complete: true, reachable: true };
|
|
2332
|
+
await roomTask(locator, async (live) => {
|
|
2333
|
+
await Promise.race([
|
|
2334
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
2335
|
+
abandonedOn(signal),
|
|
2336
|
+
]);
|
|
2337
|
+
});
|
|
2338
|
+
if (signal.aborted)
|
|
2339
|
+
return;
|
|
2340
|
+
await rest(progress);
|
|
2341
|
+
}
|
|
2342
|
+
function awaitingAnswer(locator) {
|
|
2343
|
+
let release = () => { };
|
|
2344
|
+
const awaited = new Promise((resolve) => {
|
|
2345
|
+
release = resolve;
|
|
2346
|
+
});
|
|
2347
|
+
answering.set(locator, { generation, answered: awaited });
|
|
2348
|
+
return () => {
|
|
2349
|
+
if (answering.get(locator)?.answered === awaited)
|
|
2350
|
+
answering.delete(locator);
|
|
2351
|
+
release();
|
|
2352
|
+
};
|
|
1373
2353
|
}
|
|
1374
2354
|
async function retainModel(local, room, attached, next) {
|
|
1375
2355
|
const key = modelKey(room.locator, attached);
|
|
1376
|
-
if (next.status === 'unavailable')
|
|
1377
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
1378
|
-
const epoch = modelEpoch.get(key) ?? 0;
|
|
1379
2356
|
const previous = modelOf(room.locator, attached);
|
|
1380
2357
|
const heldBefore = replicas.get(room.locator)?.confirmed.record;
|
|
1381
2358
|
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
1382
2359
|
(heldBefore === undefined ||
|
|
1383
2360
|
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
1384
2361
|
JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
|
|
1385
|
-
JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
|
|
1386
2362
|
JSON.stringify(heldBefore.typedSelection) ===
|
|
1387
|
-
JSON.stringify(room.typedSelection ?? null)
|
|
1388
|
-
JSON.stringify(heldBefore.untypedFolders) === JSON.stringify(room.untypedFolders ?? []))))
|
|
2363
|
+
JSON.stringify(room.typedSelection ?? null))))
|
|
1389
2364
|
return;
|
|
1390
2365
|
if (next.status === 'unavailable') {
|
|
1391
2366
|
invalidModels.add(key);
|
|
@@ -1394,37 +2369,30 @@ export function composeRuntime(options) {
|
|
|
1394
2369
|
await exclusive(async () => {
|
|
1395
2370
|
if (!continuing())
|
|
1396
2371
|
return;
|
|
1397
|
-
if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
|
|
1398
|
-
return;
|
|
1399
2372
|
const held = replicas.get(room.locator);
|
|
1400
2373
|
if (held !== undefined) {
|
|
1401
2374
|
const declared = {
|
|
1402
2375
|
...held.confirmed.record,
|
|
1403
2376
|
operations: room.operations,
|
|
1404
2377
|
replica: room.replica,
|
|
1405
|
-
attachedModel: room.attachedModel,
|
|
1406
2378
|
typedSelection: room.typedSelection ?? null,
|
|
1407
|
-
untypedFolders: room.untypedFolders ?? [],
|
|
1408
2379
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1409
2380
|
};
|
|
1410
2381
|
const reclassified = sameMembership(declared, held.confirmed.record)
|
|
1411
|
-
?
|
|
2382
|
+
? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
|
|
1412
2383
|
: readReclassified(declared, held.confirmed);
|
|
1413
2384
|
const record = reclassified.kind === 'staged'
|
|
1414
2385
|
? declared
|
|
1415
2386
|
: {
|
|
1416
2387
|
...declared,
|
|
1417
2388
|
typedSelection: held.confirmed.record.typedSelection,
|
|
1418
|
-
untypedFolders: held.confirmed.record.untypedFolders,
|
|
1419
2389
|
};
|
|
1420
|
-
const read = reclassified.kind === 'deferred'
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
queue: held.queue,
|
|
1425
|
-
});
|
|
2390
|
+
const read = reclassified.kind === 'deferred'
|
|
2391
|
+
? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
|
|
2392
|
+
: reclassified;
|
|
2393
|
+
await local.write({ next: record, previous: held.confirmed.record });
|
|
1426
2394
|
if (read.kind === 'staged') {
|
|
1427
|
-
replicas.set(room.locator, replicaOf(read.confirmed, held.queue,
|
|
2395
|
+
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1428
2396
|
if (declarationRefusals.delete(room.locator)) {
|
|
1429
2397
|
sourceRefusals.delete(room.locator);
|
|
1430
2398
|
unreadable.delete(room.locator);
|
|
@@ -1448,79 +2416,17 @@ export function composeRuntime(options) {
|
|
|
1448
2416
|
publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
|
|
1449
2417
|
});
|
|
1450
2418
|
}
|
|
1451
|
-
async function
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
let previous = modelOf(room.locator, attached);
|
|
1456
|
-
if (previous.status !== 'unavailable' &&
|
|
1457
|
-
(declared === null || !sameModel(previous.snapshot, declared))) {
|
|
1458
|
-
previous = { status: 'unavailable', reason: 'version-changed' };
|
|
1459
|
-
await retainModel(local, room, attached, previous);
|
|
1460
|
-
if (discoveryPaused)
|
|
1461
|
-
return;
|
|
1462
|
-
}
|
|
1463
|
-
const epoch = modelEpoch.get(key) ?? 0;
|
|
1464
|
-
let answer;
|
|
1465
|
-
try {
|
|
1466
|
-
answer = await options.adapter.model(authorized, selection === null
|
|
1467
|
-
? { locator: room.locator, attached: 'collection', model: declared }
|
|
1468
|
-
: {
|
|
1469
|
-
locator: room.locator,
|
|
1470
|
-
attached: 'typed-selection',
|
|
1471
|
-
roots: selection.roots,
|
|
1472
|
-
model: declared,
|
|
1473
|
-
});
|
|
1474
|
-
}
|
|
1475
|
-
catch {
|
|
1476
|
-
answer = { kind: 'transport-uncertainty' };
|
|
1477
|
-
}
|
|
1478
|
-
if (discoveryPaused || epoch !== (modelEpoch.get(key) ?? 0))
|
|
1479
|
-
return;
|
|
1480
|
-
if (answer.kind === 'revoked-authorization') {
|
|
1481
|
-
await refuse('revoked-authorization');
|
|
1482
|
-
return;
|
|
1483
|
-
}
|
|
1484
|
-
if (answer.kind === 'transport-uncertainty') {
|
|
1485
|
-
await retainModel(local, room, attached, cached(previous));
|
|
1486
|
-
return;
|
|
1487
|
-
}
|
|
1488
|
-
if (answer.kind === 'model-unavailable') {
|
|
1489
|
-
await retainModel(local, room, attached, {
|
|
1490
|
-
status: 'unavailable',
|
|
1491
|
-
reason: answer.reason,
|
|
1492
|
-
});
|
|
1493
|
-
return;
|
|
1494
|
-
}
|
|
1495
|
-
if (declared === null || !sameModel(answer.snapshot, declared)) {
|
|
1496
|
-
await retainModel(local, room, attached, {
|
|
1497
|
-
status: 'unavailable',
|
|
1498
|
-
reason: 'version-changed',
|
|
1499
|
-
});
|
|
1500
|
-
return;
|
|
1501
|
-
}
|
|
1502
|
-
if (previous.status !== 'unavailable' &&
|
|
1503
|
-
sameModel(previous.snapshot, answer.snapshot) &&
|
|
1504
|
-
previous.snapshot.schema !== answer.snapshot.schema) {
|
|
1505
|
-
await retainModel(local, room, attached, { status: 'unavailable', reason: 'invalid' });
|
|
2419
|
+
async function scoped(result, room, local) {
|
|
2420
|
+
if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
|
|
2421
|
+
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
2422
|
+
await loseRooms([room.locator]);
|
|
1506
2423
|
return;
|
|
1507
2424
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
}
|
|
1513
|
-
await retainModel(local, room, attached, verified.verdict === 'accept'
|
|
1514
|
-
? {
|
|
1515
|
-
status: 'current',
|
|
1516
|
-
snapshot: structuredClone(answer.snapshot),
|
|
1517
|
-
verifiedWith: MD_MODEL,
|
|
1518
|
-
types: verified.model.types,
|
|
1519
|
-
}
|
|
1520
|
-
: { status: 'unavailable', reason: 'invalid' });
|
|
1521
|
-
}
|
|
1522
|
-
async function scoped(result, room, local) {
|
|
1523
|
-
scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
|
|
2425
|
+
scopedRefusals.set(room.locator, {
|
|
2426
|
+
code: result.code,
|
|
2427
|
+
fields: result.fields,
|
|
2428
|
+
retryable: result.retryable,
|
|
2429
|
+
});
|
|
1524
2430
|
let scopedAway = room;
|
|
1525
2431
|
if (result.scope === 'model')
|
|
1526
2432
|
await retainModel(local, room, 'collection', {
|
|
@@ -1546,14 +2452,39 @@ export function composeRuntime(options) {
|
|
|
1546
2452
|
emitRooms();
|
|
1547
2453
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
1548
2454
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
2455
|
+
function heldRooms(listed) {
|
|
2456
|
+
return listed.flatMap((room) => {
|
|
2457
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
2458
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
2459
|
+
return [];
|
|
2460
|
+
return [
|
|
2461
|
+
{
|
|
2462
|
+
locator: room.locator,
|
|
2463
|
+
roomVersion: record.roomVersion,
|
|
2464
|
+
documents: record.documents,
|
|
2465
|
+
revisions: record.revisions,
|
|
2466
|
+
},
|
|
2467
|
+
];
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
2471
|
+
const acquired = listed.filter(acquires);
|
|
2472
|
+
const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
|
|
2473
|
+
if (!live())
|
|
2474
|
+
return;
|
|
2475
|
+
const ready = acquired.filter((_, index) => {
|
|
1552
2476
|
const outcome = prepared[index];
|
|
1553
2477
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1554
2478
|
});
|
|
1555
|
-
|
|
1556
|
-
|
|
2479
|
+
if (attempt === 'opened') {
|
|
2480
|
+
options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
|
|
2481
|
+
handed = true;
|
|
2482
|
+
}
|
|
2483
|
+
discovering?.release();
|
|
2484
|
+
const loaded = await Promise.allSettled(ready
|
|
2485
|
+
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
2486
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
2487
|
+
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1557
2488
|
if (failed !== undefined)
|
|
1558
2489
|
throw failed.reason;
|
|
1559
2490
|
}
|
|
@@ -1570,41 +2501,65 @@ export function composeRuntime(options) {
|
|
|
1570
2501
|
progress.complete = false;
|
|
1571
2502
|
return false;
|
|
1572
2503
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
acquireModel(local, room, authorization, null),
|
|
1576
|
-
...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
|
|
1577
|
-
]);
|
|
2504
|
+
if (prior !== undefined)
|
|
2505
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1578
2506
|
if (discoveryPaused || authorization === undefined)
|
|
1579
2507
|
return false;
|
|
1580
|
-
|
|
1581
|
-
if (replicas.has(room.locator))
|
|
1582
|
-
progress.complete = false;
|
|
1583
|
-
return false;
|
|
1584
|
-
}
|
|
1585
|
-
const held = replicas.get(room.locator);
|
|
1586
|
-
if (held !== undefined && held.confirmed.record.replica.selection !== room.replica.selection) {
|
|
1587
|
-
progress.complete = false;
|
|
1588
|
-
return false;
|
|
1589
|
-
}
|
|
1590
|
-
return true;
|
|
2508
|
+
return room.replica.admission === 'admitted';
|
|
1591
2509
|
}
|
|
1592
|
-
async function
|
|
1593
|
-
if (
|
|
2510
|
+
async function caughtUp(room, local, progress, live) {
|
|
2511
|
+
if (authorization === undefined || !live() || !behind(room))
|
|
2512
|
+
return;
|
|
2513
|
+
const withheld = await advance(room, local, authorization, live);
|
|
2514
|
+
if (withheld !== null)
|
|
2515
|
+
await withhold(withheld, room, local, progress);
|
|
2516
|
+
}
|
|
2517
|
+
function revoked(locator) {
|
|
2518
|
+
return ['collection', 'typed-selection'].some((attached) => {
|
|
2519
|
+
const model = modelOf(locator, attached);
|
|
2520
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
async function loadRoom(room, local, progress, live) {
|
|
2524
|
+
if (!live() || authorization === undefined)
|
|
1594
2525
|
return;
|
|
1595
|
-
|
|
2526
|
+
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
2527
|
+
await reload(room, local, progress, live);
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1596
2531
|
if (withheld !== null) {
|
|
1597
2532
|
await withhold(withheld, room, local, progress);
|
|
1598
2533
|
return;
|
|
1599
2534
|
}
|
|
1600
|
-
await
|
|
2535
|
+
await caughtUp(room, local, progress, live);
|
|
2536
|
+
}
|
|
2537
|
+
async function reload(room, local, progress, live) {
|
|
2538
|
+
try {
|
|
2539
|
+
if (!live() || authorization === undefined)
|
|
2540
|
+
return;
|
|
2541
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2542
|
+
if (!live())
|
|
2543
|
+
return;
|
|
2544
|
+
if (bootstrapped.kind !== 'staged') {
|
|
2545
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
2549
|
+
await caughtUp(room, local, progress, live);
|
|
2550
|
+
}
|
|
2551
|
+
finally {
|
|
2552
|
+
caughtUpTo(room.locator);
|
|
2553
|
+
}
|
|
1601
2554
|
}
|
|
1602
|
-
async function
|
|
2555
|
+
async function discoverRooms(local, progress, attempt, live) {
|
|
1603
2556
|
if (closed || authorization === undefined) {
|
|
1604
2557
|
notifyReady();
|
|
1605
2558
|
return;
|
|
1606
2559
|
}
|
|
1607
2560
|
moveTo(transferring(status));
|
|
2561
|
+
discoveries += 1;
|
|
2562
|
+
const started = discoveries;
|
|
1608
2563
|
let result;
|
|
1609
2564
|
try {
|
|
1610
2565
|
result = await options.adapter.discover(authorization);
|
|
@@ -1612,7 +2567,8 @@ export function composeRuntime(options) {
|
|
|
1612
2567
|
catch {
|
|
1613
2568
|
result = { kind: 'transport-uncertainty' };
|
|
1614
2569
|
}
|
|
1615
|
-
if (
|
|
2570
|
+
if (!live()) {
|
|
2571
|
+
progress.complete = false;
|
|
1616
2572
|
notifyReady();
|
|
1617
2573
|
return;
|
|
1618
2574
|
}
|
|
@@ -1625,12 +2581,17 @@ export function composeRuntime(options) {
|
|
|
1625
2581
|
}
|
|
1626
2582
|
}
|
|
1627
2583
|
progress.reachable = false;
|
|
2584
|
+
progress.complete = false;
|
|
1628
2585
|
notifyReady();
|
|
1629
2586
|
return;
|
|
1630
2587
|
}
|
|
1631
2588
|
if (result.kind === 'contract-unavailable') {
|
|
1632
2589
|
journal.record(`connection contract unavailable ${result.code}`);
|
|
1633
|
-
await refuse('missing-required-guarantee', {
|
|
2590
|
+
await refuse('missing-required-guarantee', {
|
|
2591
|
+
code: result.code,
|
|
2592
|
+
fields: result.fields,
|
|
2593
|
+
retryable: result.retryable,
|
|
2594
|
+
});
|
|
1634
2595
|
notifyReady();
|
|
1635
2596
|
return;
|
|
1636
2597
|
}
|
|
@@ -1639,17 +2600,6 @@ export function composeRuntime(options) {
|
|
|
1639
2600
|
notifyReady();
|
|
1640
2601
|
return;
|
|
1641
2602
|
}
|
|
1642
|
-
if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
|
|
1643
|
-
await refuse('no-matching-version');
|
|
1644
|
-
notifyReady();
|
|
1645
|
-
return;
|
|
1646
|
-
}
|
|
1647
|
-
if (result.mdModel.identity !== MD_MODEL.identity ||
|
|
1648
|
-
result.mdModel.version !== MD_MODEL.version) {
|
|
1649
|
-
await refuse('md-model-version-mismatch');
|
|
1650
|
-
notifyReady();
|
|
1651
|
-
return;
|
|
1652
|
-
}
|
|
1653
2603
|
for (const room of result.rooms) {
|
|
1654
2604
|
if (room.operations === undefined ||
|
|
1655
2605
|
!['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
@@ -1657,8 +2607,7 @@ export function composeRuntime(options) {
|
|
|
1657
2607
|
room.replica === undefined ||
|
|
1658
2608
|
typeof room.replica.selection !== 'string' ||
|
|
1659
2609
|
room.replica.selection.length === 0 ||
|
|
1660
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
1661
|
-
!declaredIdentity(room.attachedModel)) {
|
|
2610
|
+
!['admitted', 'unavailable'].includes(room.replica.admission)) {
|
|
1662
2611
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
1663
2612
|
await refuse('missing-required-guarantee');
|
|
1664
2613
|
notifyReady();
|
|
@@ -1670,20 +2619,6 @@ export function composeRuntime(options) {
|
|
|
1670
2619
|
notifyReady();
|
|
1671
2620
|
return;
|
|
1672
2621
|
}
|
|
1673
|
-
if (!declaredUntypedFolders(room)) {
|
|
1674
|
-
journal.record(`invalid untyped folder declaration ${room.locator}`);
|
|
1675
|
-
await refuse('missing-required-guarantee');
|
|
1676
|
-
notifyReady();
|
|
1677
|
-
return;
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
const declared = new Set(result.capabilities.map((capability) => capability.name));
|
|
1681
|
-
for (const required of options.requiredGuarantees) {
|
|
1682
|
-
if (!knownCapabilities.has(required) || !declared.has(required)) {
|
|
1683
|
-
await refuse('missing-required-guarantee');
|
|
1684
|
-
notifyReady();
|
|
1685
|
-
return;
|
|
1686
|
-
}
|
|
1687
2622
|
}
|
|
1688
2623
|
if (refusal !== null) {
|
|
1689
2624
|
refusal = null;
|
|
@@ -1691,30 +2626,20 @@ export function composeRuntime(options) {
|
|
|
1691
2626
|
}
|
|
1692
2627
|
const previousCapabilities = capabilities;
|
|
1693
2628
|
capabilities = result.capabilities;
|
|
2629
|
+
outcomeRetention = result.retention.outcome;
|
|
1694
2630
|
notifyReady();
|
|
1695
|
-
if (declared.has('room-version-notifications'))
|
|
1696
|
-
observeRoomVersions();
|
|
1697
|
-
const discoveredSlots = result.slots ?? [];
|
|
1698
2631
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1699
|
-
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)
|
|
1700
|
-
JSON.stringify(discoveredSlots) !== JSON.stringify(slots)) {
|
|
2632
|
+
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
1701
2633
|
limits = result.providerLimits;
|
|
1702
|
-
|
|
1703
|
-
await local.declare(limits, capabilities, slots);
|
|
2634
|
+
await local.declare(limits, capabilities);
|
|
1704
2635
|
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
.
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
await synchronize(result.rooms, local, progress);
|
|
1712
|
-
if (closed || discoveryPaused)
|
|
1713
|
-
return;
|
|
1714
|
-
await conversations.reconcile();
|
|
1715
|
-
if (status.providerUnreachable)
|
|
1716
|
-
conversations.reconnected();
|
|
1717
|
-
await conversations.catchUp(progress);
|
|
2636
|
+
const signedIn = result.member ?? null;
|
|
2637
|
+
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
2638
|
+
await local.keepMember(signedIn);
|
|
2639
|
+
member = signedIn;
|
|
2640
|
+
emitMember();
|
|
2641
|
+
}
|
|
2642
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
1718
2643
|
}
|
|
1719
2644
|
async function ensureAuthorization() {
|
|
1720
2645
|
if (authorization !== undefined)
|
|
@@ -1728,66 +2653,454 @@ export function composeRuntime(options) {
|
|
|
1728
2653
|
throw error;
|
|
1729
2654
|
}
|
|
1730
2655
|
}
|
|
1731
|
-
async function
|
|
1732
|
-
if (
|
|
1733
|
-
|
|
1734
|
-
return;
|
|
2656
|
+
async function admittedRead(work) {
|
|
2657
|
+
if (roomReads.generation !== generation) {
|
|
2658
|
+
roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
1735
2659
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
2660
|
+
const reads = roomReads;
|
|
2661
|
+
while (reads.free === 0)
|
|
2662
|
+
await new Promise((admit) => reads.waiting.push(admit));
|
|
2663
|
+
reads.free -= 1;
|
|
1739
2664
|
try {
|
|
1740
|
-
|
|
1741
|
-
|
|
2665
|
+
await work();
|
|
2666
|
+
}
|
|
2667
|
+
finally {
|
|
2668
|
+
reads.free += 1;
|
|
2669
|
+
reads.waiting.shift()?.();
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
function roomTask(locator, work) {
|
|
2673
|
+
return task(roomLane(locator), (live) => admittedRead(() => work(live)));
|
|
2674
|
+
}
|
|
2675
|
+
function task(lane, work) {
|
|
2676
|
+
if (stopping)
|
|
2677
|
+
return Promise.resolve();
|
|
2678
|
+
const held = lanes.get(lane);
|
|
2679
|
+
const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
|
|
2680
|
+
const turn = current.generation;
|
|
2681
|
+
const live = () => turn === generation && continuing();
|
|
2682
|
+
const run = current.chain.then(() => work(live)).catch(() => undefined);
|
|
2683
|
+
current.chain = run;
|
|
2684
|
+
lanes.set(lane, current);
|
|
2685
|
+
working.add(run);
|
|
2686
|
+
void run.finally(() => working.delete(run));
|
|
2687
|
+
return run;
|
|
2688
|
+
}
|
|
2689
|
+
function sync(attempt) {
|
|
2690
|
+
return task(OPENING_LANE, async (live) => {
|
|
2691
|
+
if (!live()) {
|
|
2692
|
+
notifyReady();
|
|
1742
2693
|
return;
|
|
1743
|
-
|
|
1744
|
-
|
|
2694
|
+
}
|
|
2695
|
+
const progress = { complete: true, reachable: true };
|
|
2696
|
+
const gate = discover();
|
|
2697
|
+
if (attempt === 'opened')
|
|
2698
|
+
opened = generation;
|
|
2699
|
+
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
2700
|
+
if (attempt === 'opened')
|
|
2701
|
+
streamed = progress;
|
|
2702
|
+
complete = false;
|
|
2703
|
+
recorded = false;
|
|
2704
|
+
moveTo(transferring(status));
|
|
2705
|
+
handed = false;
|
|
2706
|
+
try {
|
|
2707
|
+
const local = await openLocal();
|
|
2708
|
+
if (!live())
|
|
2709
|
+
return;
|
|
2710
|
+
await ensureAuthorization();
|
|
2711
|
+
if (!live())
|
|
2712
|
+
return;
|
|
2713
|
+
await discoverRooms(local, progress, attempt, live);
|
|
2714
|
+
}
|
|
2715
|
+
catch {
|
|
2716
|
+
progress.complete = false;
|
|
2717
|
+
if (attempt === 'opened' && !handed)
|
|
2718
|
+
progress.reachable = false;
|
|
2719
|
+
}
|
|
2720
|
+
finally {
|
|
2721
|
+
gate.release();
|
|
2722
|
+
notifyReady();
|
|
2723
|
+
}
|
|
2724
|
+
if (!live())
|
|
1745
2725
|
return;
|
|
1746
|
-
|
|
1747
|
-
|
|
2726
|
+
if (attempt !== 'opened') {
|
|
2727
|
+
await finishOpening(progress, live);
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
if (handed || closed || progress.reachable)
|
|
1748
2731
|
return;
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
2732
|
+
underway = null;
|
|
2733
|
+
opened = null;
|
|
2734
|
+
journal.record('connection reopen');
|
|
2735
|
+
await rest(progress, live);
|
|
2736
|
+
options.adapter.roomStream?.reopen();
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
function rediscover() {
|
|
2740
|
+
return task(OPENING_LANE, async (live) => {
|
|
2741
|
+
if (!live() || !socketOpen)
|
|
2742
|
+
return;
|
|
2743
|
+
const progress = { complete: true, reachable: true };
|
|
2744
|
+
const gate = discover();
|
|
2745
|
+
moveTo(transferring(status));
|
|
2746
|
+
try {
|
|
2747
|
+
const local = await openLocal();
|
|
2748
|
+
if (!live())
|
|
2749
|
+
return;
|
|
2750
|
+
await ensureAuthorization();
|
|
2751
|
+
if (!live())
|
|
2752
|
+
return;
|
|
2753
|
+
await discoverRooms(local, progress, 'authorized', live);
|
|
2754
|
+
}
|
|
2755
|
+
catch {
|
|
2756
|
+
progress.complete = false;
|
|
2757
|
+
}
|
|
2758
|
+
finally {
|
|
2759
|
+
gate.release();
|
|
2760
|
+
notifyReady();
|
|
1752
2761
|
}
|
|
2762
|
+
if (!live())
|
|
2763
|
+
return;
|
|
2764
|
+
if (!progress.complete)
|
|
2765
|
+
complete = false;
|
|
2766
|
+
await rest(progress, live);
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2769
|
+
function closedSocket(revoked, lost) {
|
|
2770
|
+
const opened = socketOpen;
|
|
2771
|
+
socketOpen = false;
|
|
2772
|
+
if (lost) {
|
|
2773
|
+
reads.lost();
|
|
2774
|
+
conversations.lost();
|
|
2775
|
+
}
|
|
2776
|
+
if (revoked) {
|
|
2777
|
+
void refuse('revoked-authorization');
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
complete = false;
|
|
2781
|
+
if (streamed !== null)
|
|
2782
|
+
streamed.complete = false;
|
|
2783
|
+
moveTo({
|
|
2784
|
+
...status,
|
|
2785
|
+
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
2786
|
+
providerUnreachable: status.providerUnreachable || !opened,
|
|
2787
|
+
});
|
|
2788
|
+
}
|
|
2789
|
+
function applyOpening(expired) {
|
|
2790
|
+
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2791
|
+
const cycle = progress ?? { complete: true, reachable: true };
|
|
2792
|
+
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2793
|
+
if (opened === generation)
|
|
2794
|
+
opened = null;
|
|
2795
|
+
return task(OPENING_LANE, async (live) => {
|
|
2796
|
+
underway = null;
|
|
2797
|
+
if (!live())
|
|
2798
|
+
return;
|
|
2799
|
+
if (progress === null) {
|
|
2800
|
+
await replacing;
|
|
2801
|
+
await rest(cycle, live);
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
await roomsCarried(expired);
|
|
2805
|
+
if (!live())
|
|
2806
|
+
return;
|
|
2807
|
+
const replaced = new Set(owed);
|
|
2808
|
+
owed.clear();
|
|
2809
|
+
try {
|
|
2810
|
+
const local = await openLocal();
|
|
2811
|
+
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
2812
|
+
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2813
|
+
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2814
|
+
for (const room of reloading)
|
|
2815
|
+
catching.add(room.locator);
|
|
2816
|
+
readSide.recover();
|
|
2817
|
+
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2818
|
+
const reloaded = [...(await reloads), ...(await replacing)];
|
|
2819
|
+
if (progress.reachable) {
|
|
2820
|
+
for (const room of held) {
|
|
2821
|
+
if (!expired.includes(room.locator) && !reloading.includes(room)) {
|
|
2822
|
+
await confirmModels(room, local);
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
2827
|
+
if (failed !== undefined)
|
|
2828
|
+
throw failed.reason;
|
|
2829
|
+
}
|
|
2830
|
+
catch {
|
|
2831
|
+
progress.complete = false;
|
|
2832
|
+
progress.reachable = false;
|
|
2833
|
+
}
|
|
2834
|
+
if (live())
|
|
2835
|
+
await finishOpening(progress, live);
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
async function reloadNow(replaced, progress) {
|
|
2839
|
+
try {
|
|
2840
|
+
const local = await openLocal();
|
|
2841
|
+
return await Promise.allSettled([...discovered.values()]
|
|
2842
|
+
.filter((room) => replicas.has(room.locator) && replaced(room))
|
|
2843
|
+
.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
1753
2844
|
}
|
|
1754
2845
|
catch {
|
|
1755
|
-
progress.
|
|
1756
|
-
|
|
2846
|
+
progress.reachable = false;
|
|
2847
|
+
return [];
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
async function confirmModels(room, local) {
|
|
2851
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
2852
|
+
const model = modelOf(room.locator, attached);
|
|
2853
|
+
if (model.status === 'cached') {
|
|
2854
|
+
await retainModel(local, room, attached, { ...model, status: 'current' });
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
function submitLater(locator) {
|
|
2859
|
+
if (localSaves)
|
|
1757
2860
|
return;
|
|
2861
|
+
void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
|
|
2862
|
+
}
|
|
2863
|
+
async function submitPass(locator, signal) {
|
|
2864
|
+
const room = discovered.get(locator);
|
|
2865
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
2866
|
+
return 'done';
|
|
2867
|
+
const progress = { complete: true, reachable: true };
|
|
2868
|
+
let next;
|
|
2869
|
+
try {
|
|
2870
|
+
await ensureAuthorization();
|
|
2871
|
+
if (authorization === undefined)
|
|
2872
|
+
return 'done';
|
|
2873
|
+
next = await submitRoom(room, await openLocal(), authorization, progress, signal);
|
|
2874
|
+
}
|
|
2875
|
+
catch {
|
|
2876
|
+
progress.reachable = false;
|
|
2877
|
+
next = 'again';
|
|
2878
|
+
}
|
|
2879
|
+
if (signal.aborted)
|
|
2880
|
+
return 'done';
|
|
2881
|
+
await rest(progress);
|
|
2882
|
+
return next === 'again' && !socketOpen ? 'done' : next;
|
|
2883
|
+
}
|
|
2884
|
+
function repairLater(locator, when) {
|
|
2885
|
+
if (!socketOpen || repairing.has(locator))
|
|
2886
|
+
return;
|
|
2887
|
+
void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
|
|
2888
|
+
}
|
|
2889
|
+
async function repairPass(locator, signal) {
|
|
2890
|
+
const room = discovered.get(locator);
|
|
2891
|
+
if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
|
|
2892
|
+
return 'done';
|
|
2893
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2894
|
+
return 'done';
|
|
2895
|
+
const progress = { complete: true, reachable: true };
|
|
2896
|
+
repairing.add(locator);
|
|
2897
|
+
try {
|
|
2898
|
+
const local = await openLocal();
|
|
2899
|
+
await roomTask(locator, async (live) => {
|
|
2900
|
+
const current = () => live() && !signal.aborted;
|
|
2901
|
+
if (!current() || authorization === undefined)
|
|
2902
|
+
return;
|
|
2903
|
+
const withheld = await Promise.race([
|
|
2904
|
+
advance(room, local, authorization, current),
|
|
2905
|
+
abandonedOn(signal),
|
|
2906
|
+
]);
|
|
2907
|
+
if (withheld !== null && current())
|
|
2908
|
+
await withhold(withheld, room, local, progress);
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2911
|
+
catch {
|
|
2912
|
+
stale.add(locator);
|
|
2913
|
+
progress.reachable = false;
|
|
1758
2914
|
}
|
|
1759
2915
|
finally {
|
|
1760
|
-
|
|
1761
|
-
|
|
2916
|
+
repairing.delete(locator);
|
|
2917
|
+
}
|
|
2918
|
+
if (signal.aborted)
|
|
2919
|
+
return 'done';
|
|
2920
|
+
await rest(progress);
|
|
2921
|
+
if (!stale.has(locator)) {
|
|
2922
|
+
if ((replicas.get(locator)?.queue.length ?? 0) > 0)
|
|
2923
|
+
submitLater(locator);
|
|
2924
|
+
return 'done';
|
|
2925
|
+
}
|
|
2926
|
+
if (!repairable.has(locator))
|
|
2927
|
+
return 'done';
|
|
2928
|
+
const reset = repairable.get(locator);
|
|
2929
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2930
|
+
}
|
|
2931
|
+
function resendLater() {
|
|
2932
|
+
if (!socketOpen)
|
|
2933
|
+
return;
|
|
2934
|
+
void recovery.attempt('resend', resendPass, { again: true });
|
|
2935
|
+
}
|
|
2936
|
+
async function resendPass(signal) {
|
|
2937
|
+
if (!socketOpen || closed || stopping || discoveryPaused)
|
|
2938
|
+
return 'done';
|
|
2939
|
+
const reading = { complete: true, reachable: true };
|
|
2940
|
+
try {
|
|
2941
|
+
await conversations.resend(reading);
|
|
2942
|
+
}
|
|
2943
|
+
catch {
|
|
2944
|
+
reading.reachable = false;
|
|
2945
|
+
}
|
|
2946
|
+
if (signal.aborted)
|
|
2947
|
+
return 'done';
|
|
2948
|
+
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
2949
|
+
}
|
|
2950
|
+
function submitAll() {
|
|
2951
|
+
for (const [locator, replica] of replicas) {
|
|
2952
|
+
if (replica.queue.length > 0)
|
|
2953
|
+
submitLater(locator);
|
|
1762
2954
|
}
|
|
1763
2955
|
}
|
|
1764
|
-
async function
|
|
2956
|
+
async function finishOpening(progress, live) {
|
|
1765
2957
|
try {
|
|
1766
|
-
|
|
2958
|
+
if (!live()) {
|
|
2959
|
+
progress.complete = false;
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
reads.applied();
|
|
2963
|
+
submitAll();
|
|
2964
|
+
for (const locator of stale)
|
|
2965
|
+
if (repairable.has(locator))
|
|
2966
|
+
repairLater(locator, {});
|
|
2967
|
+
await conversations.reconcile();
|
|
2968
|
+
if (status.providerUnreachable)
|
|
2969
|
+
conversations.reconnected();
|
|
2970
|
+
const reading = { complete: true, reachable: true };
|
|
2971
|
+
if (demand.conversations)
|
|
2972
|
+
await conversations.catchUp(reading);
|
|
2973
|
+
if (progress.reachable && reading.reachable)
|
|
2974
|
+
await conversations.resend(reading);
|
|
2975
|
+
if (progress.reachable)
|
|
2976
|
+
void recovery.attempt('calls', resendCalls);
|
|
2977
|
+
if (!reading.reachable) {
|
|
2978
|
+
progress.reachable = false;
|
|
2979
|
+
resendLater();
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
catch {
|
|
2983
|
+
progress.complete = false;
|
|
1767
2984
|
}
|
|
1768
2985
|
finally {
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
void enqueue();
|
|
2986
|
+
if (live()) {
|
|
2987
|
+
complete = progress.complete;
|
|
2988
|
+
await rest(progress, live);
|
|
1773
2989
|
}
|
|
1774
2990
|
}
|
|
1775
2991
|
}
|
|
1776
|
-
function
|
|
1777
|
-
|
|
1778
|
-
if (path === undefined || withdrawNotifications !== undefined)
|
|
2992
|
+
async function rest(progress, live = () => !closed) {
|
|
2993
|
+
if (!live())
|
|
1779
2994
|
return;
|
|
1780
|
-
|
|
1781
|
-
|
|
2995
|
+
let lastCompleted = status.lastCompleted;
|
|
2996
|
+
let synced = complete && stale.size === 0 && !pending();
|
|
2997
|
+
if (synced && !recorded) {
|
|
2998
|
+
const completedAt = new Date().toISOString();
|
|
2999
|
+
try {
|
|
3000
|
+
await (await openLocal()).complete(completedAt);
|
|
3001
|
+
if (!live())
|
|
3002
|
+
return;
|
|
3003
|
+
recorded = true;
|
|
3004
|
+
lastCompleted = completedAt;
|
|
3005
|
+
}
|
|
3006
|
+
catch {
|
|
3007
|
+
synced = false;
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
moveTo({
|
|
3011
|
+
state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
|
|
3012
|
+
lastCompleted,
|
|
3013
|
+
providerUnreachable: !progress.reachable,
|
|
3014
|
+
...attention(),
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
function discover() {
|
|
3018
|
+
let release = () => { };
|
|
3019
|
+
const done = new Promise((resolve) => {
|
|
3020
|
+
release = resolve;
|
|
3021
|
+
});
|
|
3022
|
+
discovering = { generation, done, release };
|
|
3023
|
+
return discovering;
|
|
3024
|
+
}
|
|
3025
|
+
function applyRoom(locator, roomVersion) {
|
|
3026
|
+
announced.set(locator, roomVersion);
|
|
3027
|
+
return task(roomLane(locator), async (live) => {
|
|
3028
|
+
if (discovering?.generation === generation)
|
|
3029
|
+
await discovering.done;
|
|
3030
|
+
const saving = answering.get(locator);
|
|
3031
|
+
if (saving?.generation === generation)
|
|
3032
|
+
await saving.answered;
|
|
3033
|
+
if (!live() || authorization === undefined)
|
|
3034
|
+
return;
|
|
3035
|
+
const room = discovered.get(locator);
|
|
3036
|
+
const held = replicas.get(locator);
|
|
3037
|
+
if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
|
|
1782
3038
|
return;
|
|
1783
|
-
if (
|
|
1784
|
-
|
|
3039
|
+
if (held !== undefined &&
|
|
3040
|
+
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
3041
|
+
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1785
3042
|
return;
|
|
1786
3043
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
3044
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
3045
|
+
const progress = during ?? { complete: true, reachable: true };
|
|
3046
|
+
if (during === null)
|
|
3047
|
+
moveTo(transferring(status));
|
|
3048
|
+
try {
|
|
3049
|
+
const local = await openLocal();
|
|
3050
|
+
await admittedRead(async () => {
|
|
3051
|
+
if (!live() || authorization === undefined)
|
|
3052
|
+
return;
|
|
3053
|
+
const withheld = await advance(room, local, authorization, live);
|
|
3054
|
+
if (withheld !== null)
|
|
3055
|
+
await withhold(withheld, room, local, progress);
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
catch {
|
|
3059
|
+
stale.add(locator);
|
|
3060
|
+
}
|
|
3061
|
+
finally {
|
|
3062
|
+
if (during === null)
|
|
3063
|
+
await rest(progress, live);
|
|
3064
|
+
}
|
|
1789
3065
|
});
|
|
1790
3066
|
}
|
|
3067
|
+
function roomsCarried(replacing) {
|
|
3068
|
+
const replaced = new Set(replacing.map(roomLane));
|
|
3069
|
+
return Promise.all([...lanes]
|
|
3070
|
+
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
3071
|
+
.map(([, held]) => held.chain));
|
|
3072
|
+
}
|
|
3073
|
+
const receipts = {
|
|
3074
|
+
heads: async () => (await openLocal()).receivedHeads(),
|
|
3075
|
+
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
3076
|
+
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
3077
|
+
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
3078
|
+
};
|
|
3079
|
+
function observeStream() {
|
|
3080
|
+
const stream = options.adapter.roomStream;
|
|
3081
|
+
if (stream === undefined || withdrawStream !== undefined)
|
|
3082
|
+
return;
|
|
3083
|
+
withdrawStream = stream.observe((event) => {
|
|
3084
|
+
if (closed || stopping)
|
|
3085
|
+
return;
|
|
3086
|
+
if (event.kind === 'opened') {
|
|
3087
|
+
socketOpen = true;
|
|
3088
|
+
generation += 1;
|
|
3089
|
+
reads.opened();
|
|
3090
|
+
conversations.reopened();
|
|
3091
|
+
recovery.resume();
|
|
3092
|
+
void sync('opened');
|
|
3093
|
+
}
|
|
3094
|
+
else if (event.kind === 'closed')
|
|
3095
|
+
closedSocket(event.revoked, event.lost);
|
|
3096
|
+
else if (event.kind === 'opening')
|
|
3097
|
+
void applyOpening(event.expired);
|
|
3098
|
+
else if (event.kind === 'authority-changed')
|
|
3099
|
+
void rediscover();
|
|
3100
|
+
else
|
|
3101
|
+
void applyRoom(event.locator, event.roomVersion);
|
|
3102
|
+
}, receipts);
|
|
3103
|
+
}
|
|
1791
3104
|
function writesHeld() {
|
|
1792
3105
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
1793
3106
|
}
|
|
@@ -1801,19 +3114,25 @@ export function composeRuntime(options) {
|
|
|
1801
3114
|
haltedRefusal = false;
|
|
1802
3115
|
if (mode !== serviceMode) {
|
|
1803
3116
|
serviceMode = mode;
|
|
3117
|
+
queueReadiness();
|
|
1804
3118
|
journal.record(`service mode ${mode}`);
|
|
1805
3119
|
for (const listener of serviceModeListeners)
|
|
1806
3120
|
listener(mode);
|
|
1807
3121
|
}
|
|
1808
3122
|
if (releasing)
|
|
1809
|
-
|
|
3123
|
+
submitAll();
|
|
1810
3124
|
}
|
|
1811
3125
|
function observeConversations() {
|
|
1812
3126
|
const stream = options.adapter.conversations;
|
|
1813
3127
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
1814
3128
|
return;
|
|
1815
3129
|
withdrawConversations = stream.observe((event) => {
|
|
1816
|
-
|
|
3130
|
+
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
3131
|
+
queueReadiness();
|
|
3132
|
+
if (conversations.receive(event)) {
|
|
3133
|
+
void ready.then(scheduleCurrency);
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
1817
3136
|
if (event.kind !== 'gap' || conversations.current())
|
|
1818
3137
|
return;
|
|
1819
3138
|
show();
|
|
@@ -1821,129 +3140,47 @@ export function composeRuntime(options) {
|
|
|
1821
3140
|
});
|
|
1822
3141
|
}
|
|
1823
3142
|
function scheduleCurrency() {
|
|
1824
|
-
if (currencyQueued || closed || stopping)
|
|
3143
|
+
if (currencyQueued === generation || closed || stopping)
|
|
1825
3144
|
return;
|
|
1826
|
-
currencyQueued =
|
|
1827
|
-
|
|
3145
|
+
currencyQueued = generation;
|
|
3146
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
1828
3147
|
}
|
|
1829
|
-
async function runCurrency() {
|
|
1830
|
-
currencyQueued
|
|
1831
|
-
|
|
3148
|
+
async function runCurrency(live) {
|
|
3149
|
+
if (currencyQueued === generation)
|
|
3150
|
+
currencyQueued = null;
|
|
3151
|
+
if (!live())
|
|
1832
3152
|
return;
|
|
1833
3153
|
await conversations.reconcile();
|
|
1834
|
-
if (conversations.current()) {
|
|
3154
|
+
if (conversations.current() || !demand.conversations) {
|
|
1835
3155
|
show();
|
|
1836
3156
|
return;
|
|
1837
3157
|
}
|
|
1838
|
-
const
|
|
1839
|
-
|
|
1840
|
-
moveTo(transferring(resting));
|
|
3158
|
+
const progress = { complete: false, reachable: !status.providerUnreachable };
|
|
3159
|
+
moveTo(transferring(status));
|
|
1841
3160
|
try {
|
|
1842
3161
|
await conversations.catchUp(progress);
|
|
1843
3162
|
}
|
|
1844
3163
|
catch {
|
|
1845
3164
|
progress.reachable = false;
|
|
1846
3165
|
}
|
|
1847
|
-
|
|
1848
|
-
if (!closed) {
|
|
1849
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
function handSelected(locators) {
|
|
1854
|
-
const path = options.adapter.roomVersions;
|
|
1855
|
-
if (path === undefined || withdrawNotifications === undefined)
|
|
1856
|
-
return;
|
|
1857
|
-
if (handed !== undefined && JSON.stringify(locators) === JSON.stringify(handed))
|
|
1858
|
-
return;
|
|
1859
|
-
handed = locators;
|
|
1860
|
-
path.select(locators);
|
|
1861
|
-
}
|
|
1862
|
-
async function runNotified() {
|
|
1863
|
-
const announced = [...notices];
|
|
1864
|
-
notices.clear();
|
|
1865
|
-
if (announced.length === 0 || closed || stopping || discoveryPaused)
|
|
1866
|
-
return;
|
|
1867
|
-
const resting = status;
|
|
1868
|
-
const progress = { complete: false, reachable: !resting.providerUnreachable };
|
|
1869
|
-
moveTo(transferring(resting));
|
|
1870
|
-
try {
|
|
1871
|
-
const local = await openLocal();
|
|
1872
|
-
for (const [locator, roomVersion] of announced) {
|
|
1873
|
-
if (closed || discoveryPaused || authorization === undefined)
|
|
1874
|
-
return;
|
|
1875
|
-
if (handed?.includes(locator) !== true)
|
|
1876
|
-
continue;
|
|
1877
|
-
const room = discovered.get(locator);
|
|
1878
|
-
const held = replicas.get(locator);
|
|
1879
|
-
if (room === undefined)
|
|
1880
|
-
continue;
|
|
1881
|
-
if (sourceRefusals.has(locator)) {
|
|
1882
|
-
void enqueue();
|
|
1883
|
-
continue;
|
|
1884
|
-
}
|
|
1885
|
-
if (held === undefined)
|
|
1886
|
-
continue;
|
|
1887
|
-
if (held.confirmed.record.roomVersion === roomVersion)
|
|
1888
|
-
continue;
|
|
1889
|
-
const withheld = await advance(room, local, authorization);
|
|
1890
|
-
if (withheld === null)
|
|
1891
|
-
continue;
|
|
1892
|
-
if (withheld.kind === 'refused') {
|
|
1893
|
-
void enqueue();
|
|
1894
|
-
continue;
|
|
1895
|
-
}
|
|
1896
|
-
if (withheld.kind === 'scoped-refusal') {
|
|
1897
|
-
await scoped(withheld, room, local);
|
|
1898
|
-
continue;
|
|
1899
|
-
}
|
|
1900
|
-
if (withheld.kind === 'transport-uncertainty') {
|
|
1901
|
-
progress.reachable = false;
|
|
1902
|
-
continue;
|
|
1903
|
-
}
|
|
1904
|
-
if (withheld.kind === 'room-not-readable') {
|
|
1905
|
-
unreadable.add(locator);
|
|
1906
|
-
list(room, false);
|
|
1907
|
-
emitRooms();
|
|
1908
|
-
continue;
|
|
1909
|
-
}
|
|
1910
|
-
if (withheld.kind === 'revoked-authorization') {
|
|
1911
|
-
await refuse('revoked-authorization');
|
|
1912
|
-
return;
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
catch {
|
|
1917
|
-
progress.reachable = false;
|
|
1918
|
-
}
|
|
1919
|
-
finally {
|
|
1920
|
-
if (!closed) {
|
|
1921
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
function enqueue() {
|
|
1926
|
-
if (stopping)
|
|
1927
|
-
return chain;
|
|
1928
|
-
if (inflight) {
|
|
1929
|
-
queued = true;
|
|
1930
|
-
return chain;
|
|
1931
|
-
}
|
|
1932
|
-
inflight = true;
|
|
1933
|
-
chain = chain.then(run, run);
|
|
1934
|
-
return chain;
|
|
3166
|
+
await rest(progress, live);
|
|
1935
3167
|
}
|
|
1936
3168
|
function close() {
|
|
1937
3169
|
if (closed)
|
|
1938
3170
|
return;
|
|
1939
3171
|
closed = true;
|
|
1940
|
-
|
|
1941
|
-
|
|
3172
|
+
if (narrowing !== null)
|
|
3173
|
+
clearTimeout(narrowing);
|
|
3174
|
+
if (rechecking !== null)
|
|
3175
|
+
clearInterval(rechecking);
|
|
3176
|
+
readinessWatches.clear();
|
|
3177
|
+
withdrawStream?.();
|
|
3178
|
+
withdrawStream = undefined;
|
|
1942
3179
|
withdrawConversations?.();
|
|
1943
3180
|
withdrawConversations = undefined;
|
|
1944
3181
|
withdrawServiceMode?.();
|
|
1945
3182
|
conversations.close();
|
|
1946
|
-
|
|
3183
|
+
void recovery.retire();
|
|
1947
3184
|
store?.close();
|
|
1948
3185
|
void opening?.then((opened) => {
|
|
1949
3186
|
opened.close();
|
|
@@ -1953,6 +3190,7 @@ export function composeRuntime(options) {
|
|
|
1953
3190
|
storageLossListeners.clear();
|
|
1954
3191
|
statusListeners.clear();
|
|
1955
3192
|
serviceModeListeners.clear();
|
|
3193
|
+
memberListeners.clear();
|
|
1956
3194
|
readSide.close();
|
|
1957
3195
|
queueSubscriptions.clear();
|
|
1958
3196
|
roomDraftListeners.clear();
|
|
@@ -1960,8 +3198,8 @@ export function composeRuntime(options) {
|
|
|
1960
3198
|
}
|
|
1961
3199
|
async function stop() {
|
|
1962
3200
|
stopping = true;
|
|
1963
|
-
|
|
1964
|
-
await
|
|
3201
|
+
await recovery.retire();
|
|
3202
|
+
await Promise.all(working);
|
|
1965
3203
|
await mutations;
|
|
1966
3204
|
close();
|
|
1967
3205
|
}
|
|
@@ -1970,11 +3208,116 @@ export function composeRuntime(options) {
|
|
|
1970
3208
|
options.signal?.addEventListener('abort', close, { once: true });
|
|
1971
3209
|
if (options.signal?.aborted)
|
|
1972
3210
|
close();
|
|
1973
|
-
|
|
3211
|
+
observeStream();
|
|
3212
|
+
void openLocal().then(() => {
|
|
3213
|
+
if (capabilities.length > 0)
|
|
3214
|
+
notifyReady();
|
|
3215
|
+
}, () => {
|
|
1974
3216
|
notifyReady();
|
|
1975
3217
|
});
|
|
1976
|
-
|
|
1977
|
-
|
|
3218
|
+
if (options.adapter.roomStream === undefined)
|
|
3219
|
+
void sync('unstreamed');
|
|
3220
|
+
function planned(input, reach) {
|
|
3221
|
+
return planOf(input, roomFacts(reach), planReach(reach));
|
|
3222
|
+
}
|
|
3223
|
+
function watchPlan(plan, deliver) {
|
|
3224
|
+
let seeded = false;
|
|
3225
|
+
let pending = true;
|
|
3226
|
+
const shown = () => {
|
|
3227
|
+
const seed = seeds.get(plan.key);
|
|
3228
|
+
if (seed === undefined || seeded || !pending)
|
|
3229
|
+
return;
|
|
3230
|
+
seeded = true;
|
|
3231
|
+
deliver(seededResult(seed));
|
|
3232
|
+
};
|
|
3233
|
+
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
3234
|
+
seedWatchers.set(plan.key, watchers);
|
|
3235
|
+
watchers.add(shown);
|
|
3236
|
+
const stop = readSide.watch(plan, identityOf(plan), () => {
|
|
3237
|
+
const cover = covered(plan);
|
|
3238
|
+
awaitPreparation(cover);
|
|
3239
|
+
return seedBehind(plan, cover);
|
|
3240
|
+
}, (message) => {
|
|
3241
|
+
const answered = message;
|
|
3242
|
+
pending = whole(answered) && 'pending' in answered.result;
|
|
3243
|
+
if (pending && seeds.has(plan.key)) {
|
|
3244
|
+
shown();
|
|
3245
|
+
return;
|
|
3246
|
+
}
|
|
3247
|
+
seeded = false;
|
|
3248
|
+
deliver(message);
|
|
3249
|
+
});
|
|
3250
|
+
return () => {
|
|
3251
|
+
watchers.delete(shown);
|
|
3252
|
+
if (watchers.size === 0)
|
|
3253
|
+
seedWatchers.delete(plan.key);
|
|
3254
|
+
stop();
|
|
3255
|
+
};
|
|
3256
|
+
}
|
|
3257
|
+
async function searched(input, reach) {
|
|
3258
|
+
const plan = searchPlanOf(input);
|
|
3259
|
+
const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
|
|
3260
|
+
for (;;) {
|
|
3261
|
+
const cover = covered(filter);
|
|
3262
|
+
awaitPreparation(cover);
|
|
3263
|
+
const pending = 'pending' in cover ? [...cover.pending] : [];
|
|
3264
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3265
|
+
const held = covered({
|
|
3266
|
+
...filter,
|
|
3267
|
+
rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
|
|
3268
|
+
});
|
|
3269
|
+
const covering = 'pending' in held ? [] : held.coverage.rooms;
|
|
3270
|
+
const unindexed = covering
|
|
3271
|
+
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
3272
|
+
.map(({ room }) => room);
|
|
3273
|
+
for (const room of unindexed)
|
|
3274
|
+
pending.push({ room, awaits: 'index' });
|
|
3275
|
+
const listed = conversations.searchable();
|
|
3276
|
+
const named = reach !== null && !reach.conversations
|
|
3277
|
+
? []
|
|
3278
|
+
: (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
3279
|
+
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
3280
|
+
for (const conversation of waiting)
|
|
3281
|
+
pending.push({ conversation, awaits: 'index' });
|
|
3282
|
+
if (unindexed.length > 0 || waiting.length > 0)
|
|
3283
|
+
index(unindexed, waiting);
|
|
3284
|
+
const complete = pending.length === 0 && !('pending' in cover);
|
|
3285
|
+
const answering = complete
|
|
3286
|
+
? filter
|
|
3287
|
+
: {
|
|
3288
|
+
...filter,
|
|
3289
|
+
rooms: covering
|
|
3290
|
+
.filter(({ room }) => searchIndex.rooms().has(room))
|
|
3291
|
+
.map(({ room }) => room),
|
|
3292
|
+
};
|
|
3293
|
+
const answered = covered(answering);
|
|
3294
|
+
if ('pending' in answered)
|
|
3295
|
+
continue;
|
|
3296
|
+
const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
|
|
3297
|
+
const seen = searchIndex.changes();
|
|
3298
|
+
await searchIndex.caughtUp();
|
|
3299
|
+
const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
|
|
3300
|
+
if (selected !== null && 'pending' in selected)
|
|
3301
|
+
continue;
|
|
3302
|
+
if (searchIndex.changes() !== seen ||
|
|
3303
|
+
JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
3304
|
+
continue;
|
|
3305
|
+
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
|
|
3306
|
+
? new Map(answered.coverage.rooms.map(({ room }) => [
|
|
3307
|
+
room,
|
|
3308
|
+
replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
|
|
3309
|
+
]))
|
|
3310
|
+
: null);
|
|
3311
|
+
const coverage = {
|
|
3312
|
+
rooms: answered.coverage.rooms,
|
|
3313
|
+
conversations: searchedConversations,
|
|
3314
|
+
index: searchIndex.generation(),
|
|
3315
|
+
overlay: answered.coverage.overlay,
|
|
3316
|
+
};
|
|
3317
|
+
return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
const watch = (topic, input, deliver) => {
|
|
1978
3321
|
if (topic === 'room-drafts') {
|
|
1979
3322
|
const listener = { notify: deliver, ready: false };
|
|
1980
3323
|
roomDraftListeners.add(listener);
|
|
@@ -1995,6 +3338,10 @@ export function composeRuntime(options) {
|
|
|
1995
3338
|
if (topic === 'conversation') {
|
|
1996
3339
|
return conversations.watch(input[0], input[1], deliver);
|
|
1997
3340
|
}
|
|
3341
|
+
if (topic === 'conversations')
|
|
3342
|
+
return conversations.watchList(deliver);
|
|
3343
|
+
if (topic === 'agents')
|
|
3344
|
+
return conversations.watchAgents(deliver);
|
|
1998
3345
|
if (topic === 'presentation') {
|
|
1999
3346
|
const listener = {
|
|
2000
3347
|
app: input[0],
|
|
@@ -2014,6 +3361,9 @@ export function composeRuntime(options) {
|
|
|
2014
3361
|
presentationListeners.delete(listener);
|
|
2015
3362
|
};
|
|
2016
3363
|
}
|
|
3364
|
+
if (topic === 'readiness') {
|
|
3365
|
+
return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
|
|
3366
|
+
}
|
|
2017
3367
|
if (topic === 'rooms') {
|
|
2018
3368
|
roomListeners.add(deliver);
|
|
2019
3369
|
deliver(listed());
|
|
@@ -2049,6 +3399,13 @@ export function composeRuntime(options) {
|
|
|
2049
3399
|
serviceModeListeners.delete(deliver);
|
|
2050
3400
|
};
|
|
2051
3401
|
}
|
|
3402
|
+
if (topic === 'member') {
|
|
3403
|
+
memberListeners.add(deliver);
|
|
3404
|
+
deliver(member);
|
|
3405
|
+
return () => {
|
|
3406
|
+
memberListeners.delete(deliver);
|
|
3407
|
+
};
|
|
3408
|
+
}
|
|
2052
3409
|
if (topic === 'queue') {
|
|
2053
3410
|
const queue = { locator: input[0], notify: deliver };
|
|
2054
3411
|
queueSubscriptions.add(queue);
|
|
@@ -2057,9 +3414,8 @@ export function composeRuntime(options) {
|
|
|
2057
3414
|
queueSubscriptions.delete(queue);
|
|
2058
3415
|
};
|
|
2059
3416
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
}
|
|
3417
|
+
return watchPlan(planned(input[0], null), deliver);
|
|
3418
|
+
};
|
|
2063
3419
|
const md = {
|
|
2064
3420
|
loaded() {
|
|
2065
3421
|
return openLocal().then(() => undefined);
|
|
@@ -2067,18 +3423,8 @@ export function composeRuntime(options) {
|
|
|
2067
3423
|
rooms() {
|
|
2068
3424
|
return listed();
|
|
2069
3425
|
},
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
},
|
|
2073
|
-
query: ((input) => readSide.query(planOf(input, roomFacts()))),
|
|
2074
|
-
async search(input) {
|
|
2075
|
-
const plan = searchPlanOf(input);
|
|
2076
|
-
const selected = plan.documents === undefined
|
|
2077
|
-
? null
|
|
2078
|
-
: (await readSide.query(filterOf(plan.documents, roomFacts())))
|
|
2079
|
-
.documents;
|
|
2080
|
-
return searchIndex.search(plan, selected);
|
|
2081
|
-
},
|
|
3426
|
+
query: ((input) => rowsQuery(planned(input, null))),
|
|
3427
|
+
search: (input) => searched(input, null),
|
|
2082
3428
|
types(locator) {
|
|
2083
3429
|
return {
|
|
2084
3430
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -2088,12 +3434,6 @@ export function composeRuntime(options) {
|
|
|
2088
3434
|
: modelTypes(modelOf(locator, 'typed-selection')),
|
|
2089
3435
|
};
|
|
2090
3436
|
},
|
|
2091
|
-
syncStatus() {
|
|
2092
|
-
return shown;
|
|
2093
|
-
},
|
|
2094
|
-
serviceMode() {
|
|
2095
|
-
return serviceMode;
|
|
2096
|
-
},
|
|
2097
3437
|
roomDraft: {
|
|
2098
3438
|
list() {
|
|
2099
3439
|
return exclusive(async () => {
|
|
@@ -2112,20 +3452,20 @@ export function composeRuntime(options) {
|
|
|
2112
3452
|
return exclusive(async () => {
|
|
2113
3453
|
const parts = readParts(input);
|
|
2114
3454
|
if (path === '' || normalize(path) !== path)
|
|
2115
|
-
throw
|
|
3455
|
+
throw runtimeError('room-draft-malformed');
|
|
2116
3456
|
const local = await openLocal();
|
|
2117
3457
|
const current = roomDrafts.get(room)?.get(path);
|
|
2118
3458
|
if ((current?.revision ?? null) !== revision) {
|
|
2119
|
-
return
|
|
2120
|
-
outcome: 'stale',
|
|
2121
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2122
|
-
};
|
|
3459
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2123
3460
|
}
|
|
3461
|
+
const file = parts.file === undefined
|
|
3462
|
+
? undefined
|
|
3463
|
+
: await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
|
|
2124
3464
|
const record = {
|
|
2125
3465
|
locator: room,
|
|
2126
3466
|
path,
|
|
2127
3467
|
revision: roomDraftRevision(),
|
|
2128
|
-
parts: held(current?.parts, parts),
|
|
3468
|
+
parts: held(current?.parts, parts, file),
|
|
2129
3469
|
returned: current?.returned ?? null,
|
|
2130
3470
|
};
|
|
2131
3471
|
await local.keepRoomDraft(record);
|
|
@@ -2139,10 +3479,7 @@ export function composeRuntime(options) {
|
|
|
2139
3479
|
const local = await openLocal();
|
|
2140
3480
|
const current = roomDrafts.get(room)?.get(path);
|
|
2141
3481
|
if (current === undefined || current.revision !== revision) {
|
|
2142
|
-
return
|
|
2143
|
-
outcome: 'stale',
|
|
2144
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2145
|
-
};
|
|
3482
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2146
3483
|
}
|
|
2147
3484
|
await local.discardRoomDraft(room, path);
|
|
2148
3485
|
forgetRoomDraft(room, path);
|
|
@@ -2174,14 +3511,15 @@ export function composeRuntime(options) {
|
|
|
2174
3511
|
save(room, named, writtenBy) {
|
|
2175
3512
|
return exclusive(() => saveRoomDrafts(room, named, writtenBy));
|
|
2176
3513
|
},
|
|
2177
|
-
outcome(identifier) {
|
|
2178
|
-
return outcomes.get(identifier) ?? null;
|
|
2179
|
-
},
|
|
2180
3514
|
history(locator, path, query) {
|
|
2181
3515
|
return readHistory(locator, path, query);
|
|
2182
3516
|
},
|
|
3517
|
+
binaryFile: {
|
|
3518
|
+
read: readBinaryPath,
|
|
3519
|
+
readReference: readBinaryReference,
|
|
3520
|
+
},
|
|
2183
3521
|
call(name, input) {
|
|
2184
|
-
return
|
|
3522
|
+
return recoveredAction(name, input);
|
|
2185
3523
|
},
|
|
2186
3524
|
reached(locator, roomVersion, path) {
|
|
2187
3525
|
return path === undefined
|
|
@@ -2191,25 +3529,56 @@ export function composeRuntime(options) {
|
|
|
2191
3529
|
log() {
|
|
2192
3530
|
return journal.export();
|
|
2193
3531
|
},
|
|
2194
|
-
storageLoss() {
|
|
2195
|
-
return storageLoss;
|
|
2196
|
-
},
|
|
2197
3532
|
persistence() {
|
|
2198
|
-
return
|
|
3533
|
+
return persistence === 'granted';
|
|
3534
|
+
},
|
|
3535
|
+
async retry() {
|
|
3536
|
+
journal.record('retry');
|
|
3537
|
+
unindexable.clear();
|
|
3538
|
+
queueReadiness();
|
|
3539
|
+
resume();
|
|
2199
3540
|
},
|
|
2200
|
-
observe: (
|
|
2201
|
-
deliver(structuredClone(message));
|
|
2202
|
-
}), topic === 'query')),
|
|
3541
|
+
observe: observing(watch),
|
|
2203
3542
|
async reauthorize() {
|
|
2204
3543
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2205
3544
|
return;
|
|
2206
3545
|
journal.record('connection reauthorize');
|
|
2207
|
-
await
|
|
3546
|
+
await Promise.all([...lanes.values()]
|
|
3547
|
+
.filter((held) => held.generation === generation)
|
|
3548
|
+
.map((held) => held.chain));
|
|
2208
3549
|
await mutations;
|
|
2209
3550
|
authorization = undefined;
|
|
2210
3551
|
discoveryPaused = false;
|
|
2211
|
-
|
|
3552
|
+
if (options.adapter.roomStream === undefined)
|
|
3553
|
+
await sync('unstreamed');
|
|
3554
|
+
else
|
|
3555
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
3556
|
+
},
|
|
3557
|
+
};
|
|
3558
|
+
const reachable = {
|
|
3559
|
+
md,
|
|
3560
|
+
watch,
|
|
3561
|
+
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
3562
|
+
plan: planned,
|
|
3563
|
+
query: (plan) => rowsQuery(plan),
|
|
3564
|
+
watchPlan,
|
|
3565
|
+
search: searched,
|
|
3566
|
+
watchReadiness,
|
|
3567
|
+
};
|
|
3568
|
+
return {
|
|
3569
|
+
md,
|
|
3570
|
+
seed,
|
|
3571
|
+
stop,
|
|
3572
|
+
lose: loseRooms,
|
|
3573
|
+
watch,
|
|
3574
|
+
acquire,
|
|
3575
|
+
persisted(answer) {
|
|
3576
|
+
if (answer === persistence)
|
|
3577
|
+
return;
|
|
3578
|
+
persistence = answer;
|
|
3579
|
+
journal.record(`persistence ${answer}`);
|
|
3580
|
+
queueReadiness();
|
|
2212
3581
|
},
|
|
3582
|
+
within: (reach, port) => narrowed(reachable, reach, port),
|
|
2213
3583
|
};
|
|
2214
|
-
return { md, stop, watch };
|
|
2215
3584
|
}
|