@agentshouse/mdruntime 0.1.0-c3903a68dcbbf → 0.1.0-c4478f1a5ac2a
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 +196 -97
- package/README.md +22 -12
- package/dist/adapter.d.ts +149 -64
- 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 +20 -6
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +110 -38
- package/dist/change-set.d.ts +2 -0
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +49 -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 +60 -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 +26 -0
- package/dist/chat/list.d.ts.map +1 -0
- package/dist/chat/list.js +62 -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 +21 -0
- package/dist/chat/messages.d.ts.map +1 -0
- package/dist/chat/messages.js +151 -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 +620 -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 +33 -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 +23 -19
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +69 -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 +36 -38
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1605 -841
- package/dist/conversation.d.ts +43 -19
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +523 -210
- 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 +11 -3
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +54 -69
- 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 +12 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/memory.d.ts +15 -7
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +147 -47
- package/dist/model.d.ts +14 -12
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +15 -3
- 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 +15 -3
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +94 -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 +40 -6
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +78 -36
- package/dist/reach.d.ts +42 -0
- package/dist/reach.d.ts.map +1 -0
- package/dist/reach.js +294 -0
- package/dist/react.d.ts +41 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +64 -0
- package/dist/read-side.d.ts +16 -5
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +405 -137
- 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 +12 -1
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +45 -36
- package/dist/replica.d.ts +10 -7
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +122 -79
- package/dist/room-draft.d.ts +29 -9
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +137 -26
- package/dist/rows.d.ts +10 -14
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +85 -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 +31 -8
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +109 -44
- 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 +2 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +0 -8
- package/dist/store.d.ts +71 -32
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +509 -221
- 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 +58 -19
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +65 -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,95 +1,101 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { attachedModel } from './attached-model.js';
|
|
2
2
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
3
|
-
import {
|
|
3
|
+
import { observing } from './observation.js';
|
|
4
4
|
import { filterOf, identityOf, planOf, } from './query.js';
|
|
5
5
|
import { createReadSide } from './read-side.js';
|
|
6
|
+
import { markdownPath, roomOfKey } from './rows.js';
|
|
6
7
|
import { createSearchIndex, searchPlanOf, } from './search.js';
|
|
7
|
-
import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES, } from './adapter.js';
|
|
8
8
|
import { bootstrapRoom } from './bootstrap.js';
|
|
9
|
-
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
9
|
+
import { changeSet, entering, mergeChangeSets } from './change-set.js';
|
|
10
10
|
import { createConversationStore, } from './conversation.js';
|
|
11
|
-
import {
|
|
11
|
+
import { queuedFile } from './change.js';
|
|
12
|
+
import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
|
|
13
|
+
import { runtimeError } from './failure.js';
|
|
12
14
|
import { createLog } from './log.js';
|
|
15
|
+
import { binaryRecord } from './binary.js';
|
|
16
|
+
import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
17
|
+
import { createRecovery } from './recovery.js';
|
|
13
18
|
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 {
|
|
19
|
+
import { buildBatch, joined, limitedIn, requeued } from './save.js';
|
|
20
|
+
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
|
|
21
|
+
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
22
|
+
import { same, saving, STARTING, transferring, } from './status.js';
|
|
18
23
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
19
|
-
import {
|
|
24
|
+
import { FencedMap } from './fence.js';
|
|
25
|
+
import { tally } from './shared.js';
|
|
26
|
+
import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
|
|
27
|
+
import { identityTime, roomDraftRevision } from './identity.js';
|
|
20
28
|
import { beneath, normalize } from './paths.js';
|
|
21
29
|
import { openRuntimeStore, } from './store.js';
|
|
22
30
|
import { unconfirmed } from './transfer.js';
|
|
23
|
-
|
|
24
|
-
const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
|
|
25
|
-
const EMPTY_SCHEMA = { verdict: 'accept', model: { types: {} } };
|
|
26
|
-
const knownCapabilities = new Set(VERSION_1_CAPABILITIES);
|
|
31
|
+
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
27
32
|
function within(root, path) {
|
|
28
33
|
return root === '' || beneath(root, path);
|
|
29
34
|
}
|
|
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
35
|
function declaredSelection(room) {
|
|
41
36
|
const selection = room.typedSelection;
|
|
42
37
|
if (selection === undefined)
|
|
43
38
|
return true;
|
|
44
39
|
if (!Array.isArray(selection.roots) || selection.roots.length === 0)
|
|
45
40
|
return false;
|
|
46
|
-
if (!declaredIdentity(selection.attachedModel))
|
|
47
|
-
return false;
|
|
48
41
|
return selection.roots.every((root, index) => validRoot(root) &&
|
|
49
42
|
root !== '' &&
|
|
50
43
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
51
44
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
const OPENING_LANE = 'opening';
|
|
46
|
+
const CONVERSATION_LANE = 'conversations';
|
|
47
|
+
const ROOM_READS = 256;
|
|
48
|
+
const ROOM_ATTEMPTS = 16;
|
|
49
|
+
const outside = new Map();
|
|
50
|
+
function outsideOf(roots) {
|
|
51
|
+
const key = JSON.stringify(roots);
|
|
52
|
+
const held = outside.get(key);
|
|
53
|
+
if (held !== undefined)
|
|
54
|
+
return held;
|
|
55
|
+
const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
|
|
56
|
+
const made = {
|
|
57
|
+
documents: (path) => (markdownPath(path) ? counted(path) : 0),
|
|
58
|
+
binaries: counted,
|
|
59
|
+
};
|
|
60
|
+
outside.set(key, made);
|
|
61
|
+
return made;
|
|
63
62
|
}
|
|
64
|
-
|
|
65
|
-
return
|
|
63
|
+
function abandonedOn(signal) {
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
if (signal.aborted)
|
|
66
|
+
resolve(null);
|
|
67
|
+
else
|
|
68
|
+
signal.addEventListener('abort', () => resolve(null), { once: true });
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function roomLane(locator) {
|
|
72
|
+
return `room ${locator}`;
|
|
66
73
|
}
|
|
67
74
|
export function composeRuntime(options) {
|
|
68
|
-
const
|
|
75
|
+
const localSaves = options.adapter.localSaves === true;
|
|
69
76
|
let authorization = options.authorization;
|
|
70
77
|
let opening;
|
|
71
78
|
let store;
|
|
72
|
-
const
|
|
79
|
+
const fences = new Map();
|
|
80
|
+
const isFenced = (locator) => fences.has(locator);
|
|
81
|
+
let fencings = 0;
|
|
82
|
+
const lastFenced = new Map();
|
|
83
|
+
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
84
|
+
let discoveries = 0;
|
|
85
|
+
const replicas = new FencedMap(isFenced);
|
|
86
|
+
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
73
87
|
const pendings = new Map();
|
|
74
|
-
const roomDrafts = new
|
|
88
|
+
const roomDrafts = new FencedMap(isFenced);
|
|
75
89
|
const presentations = new Map();
|
|
76
|
-
const outcomes = new Map();
|
|
77
|
-
const outcomeRooms = new Map();
|
|
78
|
-
const wanted = new Map();
|
|
79
90
|
let limits = [];
|
|
80
|
-
let slots = [];
|
|
81
91
|
let capabilities = [];
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
? 'provider'
|
|
85
|
-
: 'runtime';
|
|
86
|
-
}
|
|
87
|
-
const rooms = new Map();
|
|
92
|
+
const rooms = new FencedMap(isFenced);
|
|
93
|
+
const counted = new Map();
|
|
88
94
|
const unreadable = new Set();
|
|
89
|
-
const
|
|
90
|
-
const
|
|
95
|
+
const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
|
|
96
|
+
const models = new FencedMap(isModelFenced);
|
|
97
|
+
const liveModels = new FencedMap(isModelFenced);
|
|
91
98
|
const invalidModels = new Set();
|
|
92
|
-
const modelEpoch = new Map();
|
|
93
99
|
const discovered = new Map();
|
|
94
100
|
let roomsListed = false;
|
|
95
101
|
const scopedRefusals = new Map();
|
|
@@ -118,16 +124,29 @@ export function composeRuntime(options) {
|
|
|
118
124
|
let status = STARTING;
|
|
119
125
|
let shown = STARTING;
|
|
120
126
|
let discoveryPaused = false;
|
|
121
|
-
let timer;
|
|
122
127
|
let closed = false;
|
|
123
128
|
let stopping = false;
|
|
124
|
-
let
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
let
|
|
129
|
+
let withdrawStream;
|
|
130
|
+
const announced = new Map();
|
|
131
|
+
let underway = null;
|
|
132
|
+
let streamed = null;
|
|
133
|
+
let handed = false;
|
|
134
|
+
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
135
|
+
const repairing = new Set();
|
|
136
|
+
const repairable = new Map();
|
|
137
|
+
let outcomeRetention = null;
|
|
138
|
+
const answering = new Map();
|
|
139
|
+
let complete = false;
|
|
140
|
+
let recorded = false;
|
|
141
|
+
const stale = new Set();
|
|
142
|
+
let generation = 0;
|
|
143
|
+
let discovering = null;
|
|
144
|
+
let opened = null;
|
|
145
|
+
const owed = new Set();
|
|
146
|
+
const lanes = new Map();
|
|
147
|
+
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
148
|
+
const working = new Set();
|
|
128
149
|
let mutations = Promise.resolve();
|
|
129
|
-
let inflight = false;
|
|
130
|
-
let queued = false;
|
|
131
150
|
const roomListeners = new Set();
|
|
132
151
|
const refusalListeners = new Set();
|
|
133
152
|
const storageLossListeners = new Set();
|
|
@@ -137,26 +156,39 @@ export function composeRuntime(options) {
|
|
|
137
156
|
const presentationListeners = new Set();
|
|
138
157
|
const readSide = createReadSide();
|
|
139
158
|
let withdrawConversations;
|
|
140
|
-
let currencyQueued =
|
|
159
|
+
let currencyQueued = null;
|
|
160
|
+
let socketOpen = false;
|
|
141
161
|
let serviceMode = 'open';
|
|
142
162
|
let haltedRefusal = false;
|
|
143
163
|
let openSignals = 0;
|
|
144
164
|
const serviceModeListeners = new Set();
|
|
165
|
+
let member = null;
|
|
166
|
+
const memberListeners = new Set();
|
|
167
|
+
const prepared = new Set();
|
|
168
|
+
const projecting = new Set();
|
|
169
|
+
const demanded = new Set();
|
|
170
|
+
let preparing = false;
|
|
171
|
+
const catching = new Set();
|
|
172
|
+
const transfers = new FencedMap(isFenced);
|
|
145
173
|
const conversations = createConversationStore({
|
|
146
|
-
|
|
147
|
-
documents: (locator) =>
|
|
174
|
+
agents: declaredAgents,
|
|
175
|
+
documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
|
|
148
176
|
call: (name, input) => invokeAction(name, input),
|
|
149
177
|
durably,
|
|
150
178
|
stream: () => options.adapter.conversations,
|
|
151
179
|
publish: publishChangeSet,
|
|
180
|
+
online: () => !status.providerUnreachable && refusal === null,
|
|
152
181
|
closed: () => closed,
|
|
182
|
+
unanswered: resendLater,
|
|
183
|
+
expired: (operation) => pastHorizon(operation),
|
|
153
184
|
});
|
|
154
185
|
const searchIndex = createSearchIndex({
|
|
155
|
-
|
|
186
|
+
room: (room) => readSide.room(room),
|
|
156
187
|
document: (room, path) => readSide.document(room, path),
|
|
157
|
-
messages: () => conversations.messages(),
|
|
158
|
-
message: (conversation, identity) => conversations.message(conversation, identity),
|
|
159
188
|
});
|
|
189
|
+
const roomsToIndex = new Set();
|
|
190
|
+
const conversationsToIndex = new Set();
|
|
191
|
+
let indexing = false;
|
|
160
192
|
const journal = createLog();
|
|
161
193
|
let notifyReady = () => { };
|
|
162
194
|
const ready = new Promise((resolve) => {
|
|
@@ -220,7 +252,9 @@ export function composeRuntime(options) {
|
|
|
220
252
|
}
|
|
221
253
|
}
|
|
222
254
|
function pending() {
|
|
223
|
-
return
|
|
255
|
+
return (!localSaves &&
|
|
256
|
+
(queuesWithoutReplica.size > 0 ||
|
|
257
|
+
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
224
258
|
}
|
|
225
259
|
function attention() {
|
|
226
260
|
const attentionRequired = [];
|
|
@@ -249,23 +283,30 @@ export function composeRuntime(options) {
|
|
|
249
283
|
}
|
|
250
284
|
return { attentionRequired, reset };
|
|
251
285
|
}
|
|
286
|
+
function documentsIn(locator) {
|
|
287
|
+
const replica = replicas.get(locator);
|
|
288
|
+
if (replica === undefined)
|
|
289
|
+
return 0;
|
|
290
|
+
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
291
|
+
const weighed = outsideOf(roots);
|
|
292
|
+
return (tally(replica.visible.documents, weighed.documents) +
|
|
293
|
+
tally(replica.visible.binaries, weighed.binaries));
|
|
294
|
+
}
|
|
252
295
|
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
|
-
})));
|
|
296
|
+
return structuredClone([...rooms.values()].map((room) => {
|
|
297
|
+
const documents = documentsIn(room.locator);
|
|
298
|
+
counted.set(room.locator, documents);
|
|
299
|
+
return { ...room, documents };
|
|
300
|
+
}));
|
|
263
301
|
}
|
|
264
|
-
function roomFacts() {
|
|
265
|
-
const held = [...rooms.values()]
|
|
302
|
+
function roomFacts(reach = null) {
|
|
303
|
+
const held = [...rooms.values()]
|
|
304
|
+
.filter((room) => holds(reach, room.locator))
|
|
305
|
+
.map((room) => {
|
|
306
|
+
const governed = governs(reach, room);
|
|
266
307
|
const attached = [
|
|
267
|
-
modelOf(room.locator),
|
|
268
|
-
...(
|
|
308
|
+
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
309
|
+
...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
|
|
269
310
|
];
|
|
270
311
|
return {
|
|
271
312
|
locator: room.locator,
|
|
@@ -276,6 +317,7 @@ export function composeRuntime(options) {
|
|
|
276
317
|
return { listed: roomsListed, rooms: held };
|
|
277
318
|
}
|
|
278
319
|
function emitRooms() {
|
|
320
|
+
readSide.recover();
|
|
279
321
|
const value = listed();
|
|
280
322
|
for (const listener of roomListeners)
|
|
281
323
|
listener(value);
|
|
@@ -288,6 +330,10 @@ export function composeRuntime(options) {
|
|
|
288
330
|
for (const listener of storageLossListeners)
|
|
289
331
|
listener(storageLoss);
|
|
290
332
|
}
|
|
333
|
+
function emitMember() {
|
|
334
|
+
for (const listener of memberListeners)
|
|
335
|
+
listener(member);
|
|
336
|
+
}
|
|
291
337
|
function moveTo(next) {
|
|
292
338
|
if (closed)
|
|
293
339
|
return;
|
|
@@ -306,14 +352,17 @@ export function composeRuntime(options) {
|
|
|
306
352
|
for (const listener of statusListeners)
|
|
307
353
|
listener(shown);
|
|
308
354
|
}
|
|
309
|
-
function
|
|
310
|
-
|
|
355
|
+
function entryOf(batch) {
|
|
356
|
+
if (localSaves)
|
|
357
|
+
return Object.freeze({ batch, state: 'local' });
|
|
358
|
+
const state = pendings.get(batch.id);
|
|
359
|
+
if (state?.accepted !== undefined) {
|
|
360
|
+
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
361
|
+
}
|
|
362
|
+
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
311
363
|
}
|
|
312
364
|
function entriesOf(locator) {
|
|
313
|
-
|
|
314
|
-
if (replica === undefined)
|
|
315
|
-
return [];
|
|
316
|
-
return replica.queue.map((batch) => Object.freeze({ batch, ...submissionOf(batch) }));
|
|
365
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
317
366
|
}
|
|
318
367
|
function emitQueue(locator) {
|
|
319
368
|
const entries = entriesOf(locator);
|
|
@@ -323,12 +372,191 @@ export function composeRuntime(options) {
|
|
|
323
372
|
}
|
|
324
373
|
}
|
|
325
374
|
function applyChangeSet(set) {
|
|
326
|
-
|
|
327
|
-
|
|
375
|
+
const held = {
|
|
376
|
+
...set,
|
|
377
|
+
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
378
|
+
};
|
|
379
|
+
readSide.apply(held);
|
|
380
|
+
searchIndex.changed(held);
|
|
381
|
+
}
|
|
382
|
+
async function referenced(locator) {
|
|
383
|
+
for (;;) {
|
|
384
|
+
const record = replicas.get(locator)?.confirmed.record;
|
|
385
|
+
if (record === undefined || closed)
|
|
386
|
+
return;
|
|
387
|
+
const held = { text: record.documents, binaries: record.binaries };
|
|
388
|
+
const composition = compositionOf(record);
|
|
389
|
+
if (referencesIndexed(held, composition))
|
|
390
|
+
return;
|
|
391
|
+
await indexReferrers(held, composition);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async function prepareReplica(locator) {
|
|
395
|
+
await referenced(locator);
|
|
396
|
+
const replica = replicas.get(locator);
|
|
397
|
+
if (replica === undefined || closed || prepared.has(locator))
|
|
398
|
+
return;
|
|
399
|
+
const visible = replica.visible;
|
|
400
|
+
const paths = [
|
|
401
|
+
...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
|
|
402
|
+
];
|
|
403
|
+
const entered = [];
|
|
404
|
+
projecting.add(locator);
|
|
405
|
+
try {
|
|
406
|
+
await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
407
|
+
const current = replicas.get(locator);
|
|
408
|
+
if (current === undefined || closed)
|
|
409
|
+
return;
|
|
410
|
+
const set = entering(locator, current, chunk);
|
|
411
|
+
readSide.apply(set);
|
|
412
|
+
entered.push(set);
|
|
413
|
+
});
|
|
414
|
+
if (!replicas.has(locator) || closed)
|
|
415
|
+
return;
|
|
416
|
+
prepared.add(locator);
|
|
417
|
+
}
|
|
418
|
+
finally {
|
|
419
|
+
projecting.delete(locator);
|
|
420
|
+
}
|
|
421
|
+
conversations.changed(mergeChangeSets(entered));
|
|
422
|
+
readSide.recover();
|
|
423
|
+
}
|
|
424
|
+
function unprepared() {
|
|
425
|
+
return [...demanded, ...replicas.keys()].find((locator) => !prepared.has(locator));
|
|
426
|
+
}
|
|
427
|
+
function prepare(locators = []) {
|
|
428
|
+
for (const locator of locators)
|
|
429
|
+
demanded.add(locator);
|
|
430
|
+
if (preparing)
|
|
431
|
+
return;
|
|
432
|
+
preparing = true;
|
|
433
|
+
void (async () => {
|
|
434
|
+
try {
|
|
435
|
+
for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
|
|
436
|
+
await yielded();
|
|
437
|
+
if (closed)
|
|
438
|
+
return;
|
|
439
|
+
demanded.delete(waiting);
|
|
440
|
+
await prepareReplica(waiting);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
finally {
|
|
444
|
+
preparing = false;
|
|
445
|
+
}
|
|
446
|
+
})();
|
|
447
|
+
}
|
|
448
|
+
function reachedRooms(plan) {
|
|
449
|
+
const reach = plan.reach;
|
|
450
|
+
const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
451
|
+
const named = plan.rooms ??
|
|
452
|
+
(reached === null
|
|
453
|
+
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
454
|
+
: [...reached]);
|
|
455
|
+
return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
|
|
456
|
+
}
|
|
457
|
+
function awaited(locator) {
|
|
458
|
+
return rooms.has(locator) || !roomsListed;
|
|
459
|
+
}
|
|
460
|
+
function covered(plan) {
|
|
461
|
+
const locators = reachedRooms(plan);
|
|
462
|
+
if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
|
|
463
|
+
return { pending: [] };
|
|
464
|
+
}
|
|
465
|
+
const pending = [];
|
|
466
|
+
const coverage = [];
|
|
467
|
+
const overlay = [];
|
|
468
|
+
for (const locator of locators) {
|
|
469
|
+
const replica = replicas.get(locator);
|
|
470
|
+
if (replica === undefined) {
|
|
471
|
+
if (awaited(locator))
|
|
472
|
+
pending.push({ room: locator, awaits: 'acquisition' });
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (!prepared.has(locator)) {
|
|
476
|
+
pending.push({ room: locator, awaits: 'preparation' });
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
coverage.push({
|
|
480
|
+
room: locator,
|
|
481
|
+
generation: replica.confirmed.record.transfer,
|
|
482
|
+
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
483
|
+
});
|
|
484
|
+
for (const batch of replica.queue)
|
|
485
|
+
overlay.push(batch.id);
|
|
486
|
+
}
|
|
487
|
+
if (pending.length > 0)
|
|
488
|
+
return { pending };
|
|
489
|
+
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
490
|
+
}
|
|
491
|
+
function awaitPreparation(cover) {
|
|
492
|
+
if (!('pending' in cover))
|
|
493
|
+
return;
|
|
494
|
+
const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
|
|
495
|
+
if (waiting.length > 0)
|
|
496
|
+
prepare(waiting.map(({ room }) => room));
|
|
497
|
+
}
|
|
498
|
+
async function rowsQuery(plan) {
|
|
499
|
+
const before = covered(plan);
|
|
500
|
+
awaitPreparation(before);
|
|
501
|
+
if ('pending' in before)
|
|
502
|
+
return before;
|
|
503
|
+
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
504
|
+
awaitPreparation(cover);
|
|
505
|
+
if ('pending' in cover)
|
|
506
|
+
return cover;
|
|
507
|
+
return 'groups' in rows
|
|
508
|
+
? { groups: rows.groups, coverage: cover.coverage }
|
|
509
|
+
: { documents: rows.documents, coverage: cover.coverage };
|
|
510
|
+
}
|
|
511
|
+
async function indexConversation(conversation) {
|
|
512
|
+
searchIndex.indexMessages(conversation, [], false);
|
|
513
|
+
let after = null;
|
|
514
|
+
for (;;) {
|
|
515
|
+
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
516
|
+
if (closed)
|
|
517
|
+
return;
|
|
518
|
+
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
519
|
+
if (batch.length === 0)
|
|
520
|
+
return;
|
|
521
|
+
after = batch[batch.length - 1].identity;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function indexNext() {
|
|
525
|
+
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator));
|
|
526
|
+
if (room !== undefined) {
|
|
527
|
+
roomsToIndex.delete(room);
|
|
528
|
+
return () => (prepared.has(room) ? searchIndex.indexRoom(room) : undefined);
|
|
529
|
+
}
|
|
530
|
+
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
531
|
+
if (conversation === undefined)
|
|
532
|
+
return null;
|
|
533
|
+
conversationsToIndex.delete(conversation);
|
|
534
|
+
return () => indexConversation(conversation);
|
|
535
|
+
}
|
|
536
|
+
function index(rooms, named) {
|
|
537
|
+
for (const room of rooms)
|
|
538
|
+
roomsToIndex.add(room);
|
|
539
|
+
for (const conversation of named)
|
|
540
|
+
conversationsToIndex.add(conversation);
|
|
541
|
+
if (indexing)
|
|
542
|
+
return;
|
|
543
|
+
indexing = true;
|
|
544
|
+
void (async () => {
|
|
545
|
+
try {
|
|
546
|
+
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
547
|
+
await yielded();
|
|
548
|
+
await unit();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
indexing = false;
|
|
553
|
+
}
|
|
554
|
+
})().catch(() => { });
|
|
328
555
|
}
|
|
329
556
|
function publishChangeSet(set) {
|
|
330
557
|
applyChangeSet(set);
|
|
331
|
-
if (set.rooms.length > 0
|
|
558
|
+
if (set.rooms.length > 0 ||
|
|
559
|
+
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
332
560
|
emitRooms();
|
|
333
561
|
for (const locator of set.queues)
|
|
334
562
|
emitQueue(locator);
|
|
@@ -339,26 +567,8 @@ export function composeRuntime(options) {
|
|
|
339
567
|
for (const app of set.presentations)
|
|
340
568
|
emitPresentation(app);
|
|
341
569
|
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
570
|
}
|
|
360
571
|
async function refuse(reason, provider = null) {
|
|
361
|
-
stopTimer();
|
|
362
572
|
discoveryPaused = true;
|
|
363
573
|
try {
|
|
364
574
|
if (reason === 'revoked-authorization') {
|
|
@@ -366,10 +576,7 @@ export function composeRuntime(options) {
|
|
|
366
576
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
367
577
|
try {
|
|
368
578
|
const local = await openLocal();
|
|
369
|
-
await local.revokeModels([...replicas.values()].map((replica) =>
|
|
370
|
-
record: replica.confirmed.record,
|
|
371
|
-
queue: replica.queue,
|
|
372
|
-
})));
|
|
579
|
+
await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
|
|
373
580
|
}
|
|
374
581
|
catch {
|
|
375
582
|
journal.record('model revocation persistence failed');
|
|
@@ -381,7 +588,6 @@ export function composeRuntime(options) {
|
|
|
381
588
|
}
|
|
382
589
|
for (const key of keys) {
|
|
383
590
|
invalidModels.add(key);
|
|
384
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
385
591
|
models.set(key, revoked);
|
|
386
592
|
liveModels.delete(key);
|
|
387
593
|
}
|
|
@@ -394,10 +600,7 @@ export function composeRuntime(options) {
|
|
|
394
600
|
model: revoked,
|
|
395
601
|
selectionModel: revoked,
|
|
396
602
|
};
|
|
397
|
-
|
|
398
|
-
replicas.set(locator, read.kind === 'staged'
|
|
399
|
-
? replicaOf(read.confirmed, before.queue, rewriting(), before)
|
|
400
|
-
: { ...before, confirmed: { ...before.confirmed, record } });
|
|
603
|
+
replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
|
|
401
604
|
}
|
|
402
605
|
rooms.set(locator, {
|
|
403
606
|
...room,
|
|
@@ -414,7 +617,8 @@ export function composeRuntime(options) {
|
|
|
414
617
|
}
|
|
415
618
|
finally {
|
|
416
619
|
refusal = {
|
|
417
|
-
reason,
|
|
620
|
+
code: reason,
|
|
621
|
+
retryable: false,
|
|
418
622
|
reauthorizationOffered: reason === 'revoked-authorization',
|
|
419
623
|
refusal: provider,
|
|
420
624
|
};
|
|
@@ -427,36 +631,40 @@ export function composeRuntime(options) {
|
|
|
427
631
|
rooms.set(room.locator, {
|
|
428
632
|
name: room.name,
|
|
429
633
|
locator: room.locator,
|
|
634
|
+
handle: room.handle,
|
|
430
635
|
operations: room.operations,
|
|
431
636
|
replica: room.replica,
|
|
432
|
-
attachedModel: room.attachedModel,
|
|
433
637
|
model: modelTypes(modelOf(room.locator)),
|
|
434
638
|
typedSelection: selection === null
|
|
435
639
|
? null
|
|
436
640
|
: {
|
|
437
641
|
roots: selection.roots,
|
|
438
|
-
attachedModel: selection.attachedModel,
|
|
439
642
|
model: modelTypes(modelOf(room.locator, 'typed-selection')),
|
|
440
643
|
},
|
|
441
|
-
untypedFolders: room.untypedFolders ?? [],
|
|
442
644
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
443
645
|
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
444
|
-
attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
|
|
445
646
|
synchronizationAvailable,
|
|
446
647
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
447
648
|
});
|
|
448
649
|
}
|
|
449
|
-
async function applyDiscovery(
|
|
650
|
+
async function applyDiscovery(found, started) {
|
|
651
|
+
for (const room of found) {
|
|
652
|
+
const fence = fences.get(room.locator);
|
|
653
|
+
if (fence !== undefined && fence.erased && fence.at < started)
|
|
654
|
+
fences.delete(room.locator);
|
|
655
|
+
}
|
|
656
|
+
const listed = found.filter((room) => !fences.has(room.locator));
|
|
450
657
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
451
658
|
for (const locator of listedLocators)
|
|
452
659
|
scopedRefusals.delete(locator);
|
|
453
660
|
discovered.clear();
|
|
454
661
|
for (const room of listed)
|
|
455
662
|
discovered.set(room.locator, room);
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
663
|
+
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
|
|
664
|
+
if (options.adapter.exhaustiveDiscovery === true)
|
|
665
|
+
await loseRooms(unlisted);
|
|
666
|
+
else
|
|
667
|
+
unlistedKept(unlisted);
|
|
460
668
|
for (const room of listed) {
|
|
461
669
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
462
670
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -467,56 +675,89 @@ export function composeRuntime(options) {
|
|
|
467
675
|
}
|
|
468
676
|
roomsListed = true;
|
|
469
677
|
emitRooms();
|
|
678
|
+
return listed;
|
|
470
679
|
}
|
|
471
680
|
function unlistedHeld(listed) {
|
|
472
|
-
return [
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
room: true,
|
|
488
|
-
queue: true,
|
|
489
|
-
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
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);
|
|
681
|
+
return [
|
|
682
|
+
...new Set([
|
|
683
|
+
...rooms.keys(),
|
|
684
|
+
...roomDrafts.keys(),
|
|
685
|
+
...transfers.keys(),
|
|
686
|
+
...queuesWithoutReplica.keys(),
|
|
687
|
+
]),
|
|
688
|
+
].filter((locator) => !listed.has(locator));
|
|
689
|
+
}
|
|
690
|
+
function unlistedKept(unlisted) {
|
|
691
|
+
for (const locator of unlisted) {
|
|
692
|
+
const held = replicas.get(locator)?.confirmed.record;
|
|
693
|
+
if (held !== undefined)
|
|
694
|
+
list(held, false);
|
|
695
|
+
else
|
|
495
696
|
rooms.delete(locator);
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function fence(locators) {
|
|
700
|
+
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
701
|
+
const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
702
|
+
room: true,
|
|
703
|
+
queue: true,
|
|
704
|
+
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
705
|
+
}));
|
|
706
|
+
for (const locator of fresh) {
|
|
707
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
708
|
+
lastFenced.set(locator, ++fencings);
|
|
709
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
710
|
+
pendings.delete(batch.id);
|
|
711
|
+
replicas.delete(locator);
|
|
712
|
+
queuesWithoutReplica.delete(locator);
|
|
713
|
+
rooms.delete(locator);
|
|
714
|
+
roomDrafts.delete(locator);
|
|
715
|
+
transfers.delete(locator);
|
|
716
|
+
catching.delete(locator);
|
|
717
|
+
discovered.delete(locator);
|
|
718
|
+
announced.delete(locator);
|
|
719
|
+
unreadable.delete(locator);
|
|
720
|
+
stale.delete(locator);
|
|
721
|
+
repairable.delete(locator);
|
|
722
|
+
scopedRefusals.delete(locator);
|
|
723
|
+
sourceRefusals.delete(locator);
|
|
724
|
+
declarationRefusals.delete(locator);
|
|
725
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
726
|
+
const key = modelKey(locator, attached);
|
|
727
|
+
models.delete(key);
|
|
728
|
+
liveModels.delete(key);
|
|
729
|
+
invalidModels.delete(key);
|
|
509
730
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
731
|
+
journal.record(`room lost ${locator}`);
|
|
732
|
+
}
|
|
733
|
+
if (fresh.length === 0)
|
|
734
|
+
return fresh;
|
|
735
|
+
publishChangeSet(mergeChangeSets(removed));
|
|
736
|
+
for (const locator of fresh)
|
|
737
|
+
prepared.delete(locator);
|
|
738
|
+
searchIndex.forget(fresh);
|
|
739
|
+
readSide.recover();
|
|
740
|
+
moveTo({ ...status, ...attention() });
|
|
741
|
+
return fresh;
|
|
742
|
+
}
|
|
743
|
+
async function erased(local, locators) {
|
|
744
|
+
await local.erase(locators);
|
|
745
|
+
for (const locator of locators) {
|
|
746
|
+
const held = fences.get(locator);
|
|
747
|
+
if (held !== undefined)
|
|
748
|
+
held.erased = true;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
async function loseRooms(locators) {
|
|
752
|
+
const fresh = fence(locators);
|
|
753
|
+
if (fresh.length > 0)
|
|
754
|
+
await durably((local) => erased(local, fresh));
|
|
513
755
|
}
|
|
514
756
|
function restored(key, persisted) {
|
|
515
757
|
const prior = models.get(key);
|
|
516
758
|
if (prior !== undefined &&
|
|
517
759
|
prior.status !== 'unavailable' &&
|
|
518
760
|
(persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
|
|
519
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
520
761
|
invalidModels.add(key);
|
|
521
762
|
}
|
|
522
763
|
const model = persisted.status === 'unavailable' ||
|
|
@@ -531,10 +772,9 @@ export function composeRuntime(options) {
|
|
|
531
772
|
return model;
|
|
532
773
|
}
|
|
533
774
|
async function hydrate(opened) {
|
|
534
|
-
const [records, queues,
|
|
775
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
|
|
535
776
|
opened.all(),
|
|
536
777
|
opened.queues(),
|
|
537
|
-
opened.outcomes(),
|
|
538
778
|
opened.roomDrafts(),
|
|
539
779
|
opened.presentation(),
|
|
540
780
|
opened.conversations(),
|
|
@@ -542,11 +782,15 @@ export function composeRuntime(options) {
|
|
|
542
782
|
opened.lastCompleted(),
|
|
543
783
|
opened.providerLimits(),
|
|
544
784
|
opened.capabilities(),
|
|
545
|
-
opened.
|
|
785
|
+
opened.member(),
|
|
786
|
+
opened.transfers(),
|
|
546
787
|
]);
|
|
788
|
+
transfers.clear();
|
|
789
|
+
for (const record of staged)
|
|
790
|
+
transfers.set(record.locator, record);
|
|
547
791
|
limits = declared;
|
|
548
792
|
capabilities = named;
|
|
549
|
-
|
|
793
|
+
member = known;
|
|
550
794
|
roomDrafts.clear();
|
|
551
795
|
for (const record of kept)
|
|
552
796
|
rememberRoomDraft(record);
|
|
@@ -560,27 +804,20 @@ export function composeRuntime(options) {
|
|
|
560
804
|
for (const { batch, state } of queue.pending)
|
|
561
805
|
pendings.set(batch.id, state);
|
|
562
806
|
}
|
|
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
807
|
const previousReplicas = new Map(replicas);
|
|
570
808
|
replicas.clear();
|
|
809
|
+
queuesWithoutReplica.clear();
|
|
810
|
+
const replicated = new Set(records.map((record) => record.locator));
|
|
811
|
+
for (const [locator, queue] of queuedBatches) {
|
|
812
|
+
if (!replicated.has(locator))
|
|
813
|
+
queuesWithoutReplica.set(locator, queue);
|
|
814
|
+
}
|
|
571
815
|
for (const record of records) {
|
|
572
816
|
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
573
817
|
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
574
818
|
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));
|
|
819
|
+
const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
|
|
820
|
+
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
584
821
|
const existing = rooms.get(record.locator);
|
|
585
822
|
if (existing === undefined)
|
|
586
823
|
list(record, false);
|
|
@@ -588,25 +825,17 @@ export function composeRuntime(options) {
|
|
|
588
825
|
rooms.set(record.locator, {
|
|
589
826
|
...existing,
|
|
590
827
|
roomVersion: record.roomVersion,
|
|
591
|
-
attachments: record.attachments,
|
|
592
828
|
operations: record.operations,
|
|
593
829
|
replica: record.replica,
|
|
594
|
-
attachedModel: record.attachedModel,
|
|
595
830
|
model: modelTypes(model),
|
|
596
831
|
typedSelection: record.typedSelection === null
|
|
597
832
|
? null
|
|
598
833
|
: {
|
|
599
834
|
roots: record.typedSelection.roots,
|
|
600
|
-
attachedModel: record.typedSelection.attachedModel,
|
|
601
835
|
model: modelTypes(selectionModel),
|
|
602
836
|
},
|
|
603
|
-
untypedFolders: record.untypedFolders,
|
|
604
837
|
});
|
|
605
838
|
}
|
|
606
|
-
if (read.kind === 'refused') {
|
|
607
|
-
refusedSource(record, read);
|
|
608
|
-
declarationRefusals.add(record.locator);
|
|
609
|
-
}
|
|
610
839
|
}
|
|
611
840
|
return {
|
|
612
841
|
lastCompleted,
|
|
@@ -617,16 +846,26 @@ export function composeRuntime(options) {
|
|
|
617
846
|
async function readState(opened) {
|
|
618
847
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
619
848
|
applyChangeSet(read);
|
|
849
|
+
prepare();
|
|
620
850
|
if (replicas.size > 0)
|
|
621
851
|
emitRooms();
|
|
852
|
+
if (member !== null)
|
|
853
|
+
emitMember();
|
|
622
854
|
for (const locator of locators)
|
|
623
855
|
emitQueue(locator);
|
|
624
856
|
publishRoomDrafts(false);
|
|
625
857
|
return lastCompleted;
|
|
626
858
|
}
|
|
627
859
|
async function open() {
|
|
628
|
-
|
|
860
|
+
if (closed)
|
|
861
|
+
throw new Error('runtime closed');
|
|
862
|
+
const opened = await openRuntimeStore(isFenced);
|
|
629
863
|
store = opened;
|
|
864
|
+
const interrupted = await opened.fences();
|
|
865
|
+
for (const locator of interrupted) {
|
|
866
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
867
|
+
lastFenced.set(locator, ++fencings);
|
|
868
|
+
}
|
|
630
869
|
const kept = options.shell.generation;
|
|
631
870
|
const storedGeneration = await opened.generation();
|
|
632
871
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -640,6 +879,8 @@ export function composeRuntime(options) {
|
|
|
640
879
|
emitStorageLoss();
|
|
641
880
|
}
|
|
642
881
|
const lastCompleted = await readState(opened);
|
|
882
|
+
if (interrupted.length > 0)
|
|
883
|
+
await erased(opened, interrupted);
|
|
643
884
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
644
885
|
return opened;
|
|
645
886
|
}
|
|
@@ -666,10 +907,15 @@ export function composeRuntime(options) {
|
|
|
666
907
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
667
908
|
}
|
|
668
909
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
669
|
-
const
|
|
910
|
+
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
911
|
+
const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
|
|
670
912
|
const kept = queue.filter((batch) => !withdrawn.includes(batch));
|
|
913
|
+
const unqueued = withdrawn.map((batch) => ({
|
|
914
|
+
batch,
|
|
915
|
+
returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
|
|
916
|
+
}));
|
|
671
917
|
const returning = [
|
|
672
|
-
...
|
|
918
|
+
...[...unqueued].reverse(),
|
|
673
919
|
...(settling.leaving === undefined ? [] : [settling.leaving]),
|
|
674
920
|
];
|
|
675
921
|
const written = new Map();
|
|
@@ -687,131 +933,271 @@ export function composeRuntime(options) {
|
|
|
687
933
|
});
|
|
688
934
|
}
|
|
689
935
|
}
|
|
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);
|
|
936
|
+
await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
|
|
937
|
+
for (const batch of previousQueue) {
|
|
938
|
+
if (!kept.includes(batch))
|
|
939
|
+
pendings.delete(batch.id);
|
|
709
940
|
}
|
|
710
941
|
for (const record of written.values())
|
|
711
942
|
rememberRoomDraft(record);
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
943
|
+
try {
|
|
944
|
+
await local.write({
|
|
945
|
+
next: confirmed.record,
|
|
946
|
+
previous: before?.confirmed.record ?? null,
|
|
947
|
+
...(settling.stored === undefined ? {} : { stored: settling.stored }),
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
catch (error) {
|
|
951
|
+
const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
|
|
952
|
+
if (held !== null)
|
|
953
|
+
replicas.set(locator, held);
|
|
954
|
+
else if (kept.length > 0)
|
|
955
|
+
queuesWithoutReplica.set(locator, kept);
|
|
956
|
+
else
|
|
957
|
+
queuesWithoutReplica.delete(locator);
|
|
958
|
+
publishChangeSet(changeSet(locator, before ?? null, held, {
|
|
959
|
+
queue: kept.length !== previousQueue.length,
|
|
960
|
+
drafts: [...written.keys()],
|
|
961
|
+
}));
|
|
962
|
+
moveTo({ ...status, ...attention() });
|
|
963
|
+
throw error;
|
|
964
|
+
}
|
|
965
|
+
return () => {
|
|
966
|
+
queuesWithoutReplica.delete(locator);
|
|
967
|
+
for (const { batch, returned } of unqueued) {
|
|
968
|
+
const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
|
|
969
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
970
|
+
}
|
|
971
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
972
|
+
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
973
|
+
room: settling.room,
|
|
974
|
+
queue: kept.length !== previousQueue.length,
|
|
975
|
+
drafts: [...written.keys()],
|
|
976
|
+
});
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
function adoptModels(locator, record) {
|
|
980
|
+
for (const [attached, model] of [
|
|
981
|
+
['collection', record.model],
|
|
982
|
+
['typed-selection', record.selectionModel],
|
|
983
|
+
]) {
|
|
984
|
+
const key = modelKey(locator, attached);
|
|
985
|
+
models.set(key, model);
|
|
986
|
+
invalidModels.delete(key);
|
|
987
|
+
if (model.status === 'current')
|
|
988
|
+
liveModels.set(key, JSON.stringify(model.snapshot));
|
|
989
|
+
else if (model.status === 'unavailable')
|
|
990
|
+
liveModels.delete(key);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
function remodel(locator, answers, held) {
|
|
994
|
+
const taken = (answer, previous, attached) => {
|
|
995
|
+
if (answer === undefined)
|
|
996
|
+
return previous;
|
|
997
|
+
const { state, refused } = attachedModel(answer, previous);
|
|
998
|
+
if (refused !== null) {
|
|
999
|
+
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
1000
|
+
journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
|
|
1001
|
+
}
|
|
1002
|
+
models.set(modelKey(locator, attached), state);
|
|
1003
|
+
return state;
|
|
1004
|
+
};
|
|
1005
|
+
return {
|
|
1006
|
+
collection: taken(answers?.collection, held.collection, 'collection'),
|
|
1007
|
+
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
1008
|
+
};
|
|
720
1009
|
}
|
|
721
|
-
function publish(local, room, confirmed) {
|
|
1010
|
+
function publish(local, room, confirmed, landing, live) {
|
|
1011
|
+
const { base, stored } = landing;
|
|
722
1012
|
return exclusive(async () => {
|
|
723
|
-
if (
|
|
724
|
-
|
|
1013
|
+
if (stored !== undefined) {
|
|
1014
|
+
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1015
|
+
}
|
|
1016
|
+
if (!live()) {
|
|
1017
|
+
caughtUpTo(room.locator);
|
|
1018
|
+
return true;
|
|
1019
|
+
}
|
|
725
1020
|
const prior = replicas.get(room.locator);
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
const
|
|
734
|
-
const roomVersion = retained.confirmed.record.roomVersion;
|
|
1021
|
+
if (base !== undefined &&
|
|
1022
|
+
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
1023
|
+
prior.confirmed.record.revisions !== base.record.revisions)) {
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
adoptModels(room.locator, confirmed.record);
|
|
1027
|
+
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
1028
|
+
const roomVersion = confirmed.record.roomVersion;
|
|
735
1029
|
const accepted = queue.filter((batch) => {
|
|
736
|
-
const
|
|
737
|
-
return (
|
|
738
|
-
options.adapter.reached(room.locator, roomVersion, outcome.roomVersion));
|
|
1030
|
+
const accepted = pendings.get(batch.id)?.accepted;
|
|
1031
|
+
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
739
1032
|
});
|
|
740
|
-
const set = await settle(local, room.locator, prior,
|
|
1033
|
+
const set = (await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) }))();
|
|
1034
|
+
if (stored !== undefined) {
|
|
1035
|
+
transfers.delete(room.locator);
|
|
1036
|
+
catching.delete(room.locator);
|
|
1037
|
+
}
|
|
741
1038
|
for (const batch of accepted)
|
|
742
1039
|
pendings.delete(batch.id);
|
|
743
|
-
|
|
744
|
-
if (
|
|
745
|
-
|
|
746
|
-
}
|
|
747
|
-
unreadable.delete(room.locator);
|
|
1040
|
+
stale.delete(room.locator);
|
|
1041
|
+
if (unreadable.delete(room.locator))
|
|
1042
|
+
scopedRefusals.delete(room.locator);
|
|
748
1043
|
sourceRefusals.delete(room.locator);
|
|
749
1044
|
declarationRefusals.delete(room.locator);
|
|
750
1045
|
list(room, true);
|
|
1046
|
+
if (prior === undefined)
|
|
1047
|
+
prepared.add(room.locator);
|
|
751
1048
|
publishChangeSet(set);
|
|
752
1049
|
moveTo({ ...status, ...attention() });
|
|
1050
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
1051
|
+
return true;
|
|
753
1052
|
});
|
|
754
1053
|
}
|
|
755
1054
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
756
1055
|
const local = await openLocal();
|
|
1056
|
+
await referenced(locator);
|
|
757
1057
|
const replica = replicas.get(locator);
|
|
758
1058
|
const room = rooms.get(locator);
|
|
759
1059
|
if (replica === undefined || room === undefined) {
|
|
760
|
-
throw
|
|
1060
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
761
1061
|
}
|
|
762
1062
|
const saved = [];
|
|
763
1063
|
for (const { path, revision } of named) {
|
|
764
1064
|
const record = roomDrafts.get(locator)?.get(path);
|
|
765
1065
|
if (record === undefined || record.revision !== revision) {
|
|
766
|
-
return { outcome: 'refused',
|
|
1066
|
+
return { outcome: 'refused', code: 'stale', retryable: false, file: path };
|
|
767
1067
|
}
|
|
768
1068
|
if (!saved.some((draft) => draft.path === path))
|
|
769
1069
|
saved.push({ path, parts: record.parts });
|
|
770
1070
|
}
|
|
771
1071
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
772
1072
|
if (changed.length > 0)
|
|
773
|
-
return { outcome: 'refused',
|
|
1073
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
774
1074
|
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
775
|
-
const
|
|
1075
|
+
const changes = parts.flatMap(({ changes }) => changes);
|
|
1076
|
+
const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
1077
|
+
if (requeue.kind === 'refused')
|
|
1078
|
+
return { outcome: 'refused', ...requeue.refusal };
|
|
1079
|
+
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
1080
|
+
const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
|
|
1081
|
+
return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
|
|
1082
|
+
});
|
|
1083
|
+
if (foreign) {
|
|
1084
|
+
return {
|
|
1085
|
+
outcome: 'refused',
|
|
1086
|
+
code: 'unknown-revision',
|
|
1087
|
+
retryable: false,
|
|
1088
|
+
file: [...requeue.queued].at(-1),
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
const withdrawing = requeue.withdrawn.size > 0;
|
|
1092
|
+
const input = {
|
|
1093
|
+
local: localSaves,
|
|
776
1094
|
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
777
1095
|
composition: compositionFor(replica.confirmed.record),
|
|
778
1096
|
operations: room.operations,
|
|
779
1097
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
780
1098
|
providerLimits: limits,
|
|
781
|
-
|
|
1099
|
+
handle: room.handle,
|
|
782
1100
|
visible: replica.visible,
|
|
783
|
-
changes
|
|
1101
|
+
changes,
|
|
784
1102
|
bases: parts.flatMap(({ bases }) => bases),
|
|
785
1103
|
writtenBy,
|
|
1104
|
+
clean: {
|
|
1105
|
+
text: replica.confirmed.record.documents,
|
|
1106
|
+
binaries: replica.confirmed.record.binaries,
|
|
1107
|
+
composition: compositionOf(replica.confirmed.record),
|
|
1108
|
+
},
|
|
1109
|
+
};
|
|
1110
|
+
const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
|
|
1111
|
+
if (checked?.kind === 'refused')
|
|
1112
|
+
return { outcome: 'refused', ...checked.refusal };
|
|
1113
|
+
const owners = [...requeue.owners].flatMap((origin) => {
|
|
1114
|
+
const batch = requeue.withdrawn.get(origin);
|
|
1115
|
+
return batch === undefined || batch === null ? [] : [{ origin, batch }];
|
|
786
1116
|
});
|
|
787
|
-
if (
|
|
1117
|
+
if (owners.length > 1) {
|
|
1118
|
+
return {
|
|
1119
|
+
outcome: 'refused',
|
|
1120
|
+
code: 'unknown-revision',
|
|
1121
|
+
retryable: false,
|
|
1122
|
+
file: [...requeue.queued].at(-1),
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
const built = !withdrawing
|
|
1126
|
+
? buildBatch(input)
|
|
1127
|
+
: requeue.changes.length === 0
|
|
1128
|
+
? null
|
|
1129
|
+
: buildBatch({
|
|
1130
|
+
...input,
|
|
1131
|
+
visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
|
|
1132
|
+
changes: requeue.changes,
|
|
1133
|
+
});
|
|
1134
|
+
if (built?.kind === 'refused')
|
|
788
1135
|
return { outcome: 'refused', ...built.refusal };
|
|
789
|
-
const
|
|
1136
|
+
const [owner] = owners;
|
|
1137
|
+
const join = built === null || owner === undefined
|
|
1138
|
+
? null
|
|
1139
|
+
: joined(requeue.queue, owner.batch, built.batch);
|
|
1140
|
+
if (join?.kind === 'refused')
|
|
1141
|
+
return { outcome: 'refused', ...join.refusal };
|
|
1142
|
+
const withdrawn = new Map(requeue.withdrawn);
|
|
1143
|
+
if (join !== null && owner !== undefined)
|
|
1144
|
+
withdrawn.set(owner.origin, join.batch);
|
|
1145
|
+
const drafted = new Map();
|
|
1146
|
+
for (const [origin, replaced] of withdrawn) {
|
|
1147
|
+
if (replaced === null)
|
|
1148
|
+
continue;
|
|
1149
|
+
const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
|
|
1150
|
+
drafted.set(replaced.id, replaced === join?.batch
|
|
1151
|
+
? [
|
|
1152
|
+
...carried.map((held) => joinedDraft(held, saved)),
|
|
1153
|
+
...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
|
|
1154
|
+
]
|
|
1155
|
+
: carried);
|
|
1156
|
+
}
|
|
1157
|
+
if (built !== null && join === null)
|
|
1158
|
+
drafted.set(built.batch.id, saved);
|
|
1159
|
+
const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
|
|
1160
|
+
for (const replaced of withdrawn.values()) {
|
|
1161
|
+
if (replaced === null || !batches.includes(replaced))
|
|
1162
|
+
continue;
|
|
1163
|
+
const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
1164
|
+
if (limited !== null)
|
|
1165
|
+
return { outcome: 'refused', ...limited };
|
|
1166
|
+
}
|
|
790
1167
|
const consumed = saved.map(({ path }) => path);
|
|
791
1168
|
try {
|
|
792
|
-
await local.
|
|
793
|
-
|
|
794
|
-
previous: replica.confirmed.record,
|
|
1169
|
+
await local.keepAuthored({
|
|
1170
|
+
locator,
|
|
795
1171
|
queue: batches,
|
|
796
|
-
|
|
797
|
-
saved,
|
|
1172
|
+
saved: drafted,
|
|
798
1173
|
consumedRoomDrafts: consumed,
|
|
799
1174
|
});
|
|
800
1175
|
}
|
|
801
1176
|
catch {
|
|
802
|
-
return { outcome: 'refused',
|
|
1177
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
803
1178
|
}
|
|
804
|
-
|
|
1179
|
+
if (fences.has(locator))
|
|
1180
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1181
|
+
for (const origin of withdrawn.keys())
|
|
1182
|
+
pendings.delete(origin);
|
|
1183
|
+
for (const [id, drafts] of drafted) {
|
|
1184
|
+
pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
|
|
1185
|
+
}
|
|
1186
|
+
if (!localSaves)
|
|
1187
|
+
recorded = false;
|
|
805
1188
|
for (const path of consumed)
|
|
806
1189
|
forgetRoomDraft(locator, path);
|
|
807
|
-
replicas.set(locator, replicaOf(replica.confirmed, batches,
|
|
1190
|
+
replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
|
|
808
1191
|
publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
|
|
809
1192
|
queue: true,
|
|
810
1193
|
drafts: consumed,
|
|
811
1194
|
}));
|
|
812
|
-
moveTo({ ...saving(status), ...attention() });
|
|
813
|
-
|
|
814
|
-
|
|
1195
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
1196
|
+
submitLater(locator);
|
|
1197
|
+
const identifier = join?.batch.id ??
|
|
1198
|
+
built?.batch.id ??
|
|
1199
|
+
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
1200
|
+
return { outcome: 'saved', identifier };
|
|
815
1201
|
}
|
|
816
1202
|
async function mark(local, locator, identifier, submission, lost) {
|
|
817
1203
|
const state = pendings.get(identifier);
|
|
@@ -821,6 +1207,8 @@ export function composeRuntime(options) {
|
|
|
821
1207
|
return;
|
|
822
1208
|
}
|
|
823
1209
|
await local.submit(locator, identifier, submission, lost);
|
|
1210
|
+
if (fences.has(locator))
|
|
1211
|
+
return;
|
|
824
1212
|
pendings.set(identifier, { ...state, submission, lost });
|
|
825
1213
|
const replica = replicas.get(locator);
|
|
826
1214
|
if (replica !== undefined)
|
|
@@ -831,22 +1219,24 @@ export function composeRuntime(options) {
|
|
|
831
1219
|
if (!continuing())
|
|
832
1220
|
throw new Error('connection refused');
|
|
833
1221
|
await local.keepUpload(locator, identifier, upload);
|
|
1222
|
+
if (fences.has(locator))
|
|
1223
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
834
1224
|
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
835
1225
|
if (!continuing())
|
|
836
1226
|
throw new Error('connection refused');
|
|
837
1227
|
}
|
|
838
1228
|
async function readHistory(locator, path, query = {}) {
|
|
839
1229
|
if (closed || refusal !== null)
|
|
840
|
-
return
|
|
1230
|
+
return UNCERTAIN;
|
|
841
1231
|
try {
|
|
842
1232
|
await ensureAuthorization();
|
|
843
1233
|
}
|
|
844
1234
|
catch {
|
|
845
|
-
return
|
|
846
|
-
}
|
|
847
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
848
|
-
return { kind: 'transport-uncertainty' };
|
|
1235
|
+
return UNCERTAIN;
|
|
849
1236
|
}
|
|
1237
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1238
|
+
return UNCERTAIN;
|
|
1239
|
+
const since = fencings;
|
|
850
1240
|
const transfer = ['transfer', 'history', locator, path];
|
|
851
1241
|
if (query.revision !== undefined)
|
|
852
1242
|
transfer.push(query.revision);
|
|
@@ -861,6 +1251,8 @@ export function composeRuntime(options) {
|
|
|
861
1251
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
862
1252
|
});
|
|
863
1253
|
journal.record(`outcome history ${result.kind}`);
|
|
1254
|
+
if (fencedSince(locator, since))
|
|
1255
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
864
1256
|
if (result.kind === 'scoped-refusal') {
|
|
865
1257
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
866
1258
|
if (room !== undefined)
|
|
@@ -868,17 +1260,111 @@ export function composeRuntime(options) {
|
|
|
868
1260
|
}
|
|
869
1261
|
if (result.kind === 'revoked-authorization')
|
|
870
1262
|
await refuse('revoked-authorization');
|
|
871
|
-
return result;
|
|
1263
|
+
return historyAnswer(result);
|
|
872
1264
|
}
|
|
873
1265
|
catch {
|
|
874
1266
|
journal.record('outcome history transport-uncertainty');
|
|
875
|
-
return
|
|
1267
|
+
return UNCERTAIN;
|
|
876
1268
|
}
|
|
877
1269
|
}
|
|
878
|
-
function
|
|
1270
|
+
function queuedBlob(locator, path, localId) {
|
|
1271
|
+
for (const batch of replicas.get(locator)?.queue ?? []) {
|
|
1272
|
+
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
1273
|
+
if (change?.kind === 'binary')
|
|
1274
|
+
return change.blob;
|
|
1275
|
+
}
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
function binaryRoom(request) {
|
|
1279
|
+
if (request.kind === 'path')
|
|
1280
|
+
return request.locator;
|
|
1281
|
+
const scope = options.adapter.referrerScope(request.referrer);
|
|
1282
|
+
return scope?.kind === 'room' ? scope.locator : null;
|
|
1283
|
+
}
|
|
1284
|
+
async function readBinary(request) {
|
|
1285
|
+
if (closed || refusal !== null)
|
|
1286
|
+
return UNCERTAIN;
|
|
1287
|
+
try {
|
|
1288
|
+
await ensureAuthorization();
|
|
1289
|
+
}
|
|
1290
|
+
catch {
|
|
1291
|
+
return UNCERTAIN;
|
|
1292
|
+
}
|
|
1293
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1294
|
+
return UNCERTAIN;
|
|
1295
|
+
const since = fencings;
|
|
1296
|
+
journal.record(request.kind === 'path'
|
|
1297
|
+
? `transfer binary ${request.locator} ${request.path}`
|
|
1298
|
+
: `transfer binary ${request.fileRef}`);
|
|
1299
|
+
try {
|
|
1300
|
+
const result = await options.adapter.binary(authorization, request);
|
|
1301
|
+
journal.record(`outcome binary ${result.kind}`);
|
|
1302
|
+
const locator = binaryRoom(request);
|
|
1303
|
+
if (locator !== null && fencedSince(locator, since))
|
|
1304
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1305
|
+
if (result.kind === 'scoped-refusal') {
|
|
1306
|
+
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1307
|
+
if (room !== undefined)
|
|
1308
|
+
await scoped(result, room, await openLocal());
|
|
1309
|
+
}
|
|
1310
|
+
if (result.kind === 'revoked-authorization')
|
|
1311
|
+
await refuse('revoked-authorization');
|
|
1312
|
+
return binaryAnswer(result);
|
|
1313
|
+
}
|
|
1314
|
+
catch {
|
|
1315
|
+
journal.record('outcome binary transport-uncertainty');
|
|
1316
|
+
return UNCERTAIN;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
async function readBinaryPath(locator, path, revision) {
|
|
1320
|
+
await ready;
|
|
1321
|
+
if (revision !== undefined)
|
|
1322
|
+
return readBinary({ kind: 'path', locator, path, revision });
|
|
1323
|
+
const visible = replicas.get(locator)?.visible;
|
|
1324
|
+
const held = visible?.binaries[path];
|
|
1325
|
+
if (visible === undefined || held === undefined)
|
|
1326
|
+
return absentBinary(path);
|
|
1327
|
+
if (held.state === 'pending') {
|
|
1328
|
+
const blob = queuedBlob(locator, path, held.localId);
|
|
1329
|
+
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1330
|
+
}
|
|
1331
|
+
const record = binaryRecord(path, held, visible.revisions[path]);
|
|
1332
|
+
if (record.state !== 'accepted')
|
|
1333
|
+
return absentBinary(path);
|
|
1334
|
+
const replica = replicas.get(locator);
|
|
1335
|
+
const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
|
|
1336
|
+
const holds = (at) => {
|
|
1337
|
+
const kept = replica?.confirmed.record.binaries[at];
|
|
1338
|
+
return (kept?.state === 'accepted' &&
|
|
1339
|
+
kept.fileRef === record.fileRef &&
|
|
1340
|
+
replica?.confirmed.record.revisions[at] === record.revision);
|
|
1341
|
+
};
|
|
1342
|
+
let confirmed = path;
|
|
1343
|
+
for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
|
|
1344
|
+
const change = queued[index];
|
|
1345
|
+
if (change.kind === 'move' && change.to === confirmed)
|
|
1346
|
+
confirmed = change.path;
|
|
1347
|
+
}
|
|
1348
|
+
return readBinary({
|
|
1349
|
+
kind: 'path',
|
|
1350
|
+
locator,
|
|
1351
|
+
path: confirmed,
|
|
1352
|
+
revision: record.revision,
|
|
1353
|
+
fileRef: record.fileRef,
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
async function readBinaryReference(reference) {
|
|
1357
|
+
await ready;
|
|
1358
|
+
return readBinary({
|
|
1359
|
+
kind: 'reference',
|
|
1360
|
+
fileRef: reference.fileRef,
|
|
1361
|
+
referrer: reference.referrer,
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
function declaredAgents() {
|
|
879
1365
|
for (const capability of capabilities) {
|
|
880
|
-
if (capability.
|
|
881
|
-
return capability.
|
|
1366
|
+
if (capability.conversationAgents !== undefined)
|
|
1367
|
+
return capability.conversationAgents;
|
|
882
1368
|
}
|
|
883
1369
|
return undefined;
|
|
884
1370
|
}
|
|
@@ -894,23 +1380,20 @@ export function composeRuntime(options) {
|
|
|
894
1380
|
async function invokeAction(name, input) {
|
|
895
1381
|
await ready;
|
|
896
1382
|
if (closed || refusal !== null)
|
|
897
|
-
return
|
|
1383
|
+
return UNCERTAIN;
|
|
898
1384
|
if (declaredAction(name) === undefined)
|
|
899
|
-
return
|
|
1385
|
+
return undeclared(name);
|
|
900
1386
|
try {
|
|
901
1387
|
await ensureAuthorization();
|
|
902
1388
|
}
|
|
903
1389
|
catch {
|
|
904
|
-
return
|
|
905
|
-
}
|
|
906
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
907
|
-
return { kind: 'transport-uncertainty' };
|
|
1390
|
+
return UNCERTAIN;
|
|
908
1391
|
}
|
|
1392
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1393
|
+
return UNCERTAIN;
|
|
909
1394
|
journal.record(`transfer action ${name}`);
|
|
910
1395
|
try {
|
|
911
1396
|
const result = await options.adapter.action(authorization, { name, input });
|
|
912
|
-
if (result.kind === 'action' && result.at !== undefined)
|
|
913
|
-
want(result.at);
|
|
914
1397
|
if (result.kind === 'scoped-refusal') {
|
|
915
1398
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
916
1399
|
if (room !== undefined)
|
|
@@ -921,11 +1404,11 @@ export function composeRuntime(options) {
|
|
|
921
1404
|
journal.record(`outcome action ${result.kind}`);
|
|
922
1405
|
if (result.kind === 'rejected')
|
|
923
1406
|
journal.record(`refusal rejected ${result.code}`);
|
|
924
|
-
return result;
|
|
1407
|
+
return calledResult(result);
|
|
925
1408
|
}
|
|
926
1409
|
catch {
|
|
927
1410
|
journal.record('outcome action transport-uncertainty');
|
|
928
|
-
return
|
|
1411
|
+
return UNCERTAIN;
|
|
929
1412
|
}
|
|
930
1413
|
}
|
|
931
1414
|
function continuing() {
|
|
@@ -942,32 +1425,11 @@ export function composeRuntime(options) {
|
|
|
942
1425
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
943
1426
|
!options.adapter.reached(locator, roomVersion, written));
|
|
944
1427
|
}
|
|
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())
|
|
1428
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
1429
|
+
if (!live())
|
|
968
1430
|
return unconfirmed;
|
|
969
1431
|
try {
|
|
970
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed,
|
|
1432
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
971
1433
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
972
1434
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
973
1435
|
return result;
|
|
@@ -977,55 +1439,121 @@ export function composeRuntime(options) {
|
|
|
977
1439
|
throw error;
|
|
978
1440
|
}
|
|
979
1441
|
}
|
|
980
|
-
|
|
981
|
-
|
|
1442
|
+
function staging(local, live) {
|
|
1443
|
+
return {
|
|
1444
|
+
held: (locator) => transfers.get(locator) ?? null,
|
|
1445
|
+
staged: (record) => local.staged(record),
|
|
1446
|
+
stage: (record, page) => exclusive(async () => {
|
|
1447
|
+
if (!live())
|
|
1448
|
+
return false;
|
|
1449
|
+
await local.stage(record, page);
|
|
1450
|
+
transfers.set(record.locator, record);
|
|
1451
|
+
readSide.recover();
|
|
1452
|
+
return true;
|
|
1453
|
+
}),
|
|
1454
|
+
abandon: (locator) => exclusive(async () => {
|
|
1455
|
+
if (!live())
|
|
1456
|
+
return;
|
|
1457
|
+
await local.abandon(locator);
|
|
1458
|
+
transfers.delete(locator);
|
|
1459
|
+
readSide.recover();
|
|
1460
|
+
}),
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
function reusable(room) {
|
|
1464
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1465
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1466
|
+
return null;
|
|
1467
|
+
return { documents: record.documents, revisions: record.revisions };
|
|
1468
|
+
}
|
|
1469
|
+
async function pullBootstrap(authorized, room, local, live) {
|
|
1470
|
+
if (!live())
|
|
982
1471
|
return unconfirmed;
|
|
1472
|
+
if (replicas.has(room.locator)) {
|
|
1473
|
+
catching.add(room.locator);
|
|
1474
|
+
readSide.recover();
|
|
1475
|
+
}
|
|
983
1476
|
try {
|
|
984
1477
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
985
1478
|
collection: modelOf(room.locator),
|
|
986
1479
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
987
|
-
},
|
|
1480
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
988
1481
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
989
1482
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1483
|
+
if (result.kind !== 'staged')
|
|
1484
|
+
caughtUpTo(room.locator);
|
|
990
1485
|
return result;
|
|
991
1486
|
}
|
|
992
1487
|
catch (error) {
|
|
993
1488
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1489
|
+
caughtUpTo(room.locator);
|
|
994
1490
|
throw error;
|
|
995
1491
|
}
|
|
996
1492
|
}
|
|
997
|
-
|
|
1493
|
+
function caughtUpTo(locator) {
|
|
1494
|
+
if (catching.delete(locator))
|
|
1495
|
+
readSide.recover();
|
|
1496
|
+
}
|
|
1497
|
+
async function advance(room, local, authorized, live) {
|
|
1498
|
+
await referenced(room.locator);
|
|
1499
|
+
if (!live())
|
|
1500
|
+
return unconfirmed;
|
|
998
1501
|
const held = replicas.get(room.locator);
|
|
999
1502
|
if (held !== undefined) {
|
|
1000
|
-
const refreshed = await pullRefresh(authorized, held.confirmed,
|
|
1001
|
-
if (!
|
|
1503
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
1504
|
+
if (!live())
|
|
1002
1505
|
return unconfirmed;
|
|
1003
1506
|
if (refreshed.kind === 'still-current') {
|
|
1507
|
+
stale.delete(room.locator);
|
|
1004
1508
|
if (sourceRefusals.has(room.locator))
|
|
1005
|
-
return
|
|
1509
|
+
return null;
|
|
1006
1510
|
if (unreadable.delete(room.locator)) {
|
|
1511
|
+
scopedRefusals.delete(room.locator);
|
|
1007
1512
|
list(room, true);
|
|
1008
1513
|
emitRooms();
|
|
1009
1514
|
}
|
|
1010
1515
|
return null;
|
|
1011
1516
|
}
|
|
1012
1517
|
if (refreshed.kind === 'advanced') {
|
|
1013
|
-
await publish(local, room, refreshed.confirmed)
|
|
1518
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
1519
|
+
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1520
|
+
const reached = now !== undefined &&
|
|
1521
|
+
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
1522
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
1523
|
+
}
|
|
1524
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1014
1525
|
return null;
|
|
1015
1526
|
}
|
|
1016
1527
|
if (refreshed.kind !== 'beyond-horizon')
|
|
1017
1528
|
return refreshed;
|
|
1529
|
+
if (opened !== null) {
|
|
1530
|
+
owed.add(room.locator);
|
|
1531
|
+
readSide.recover();
|
|
1532
|
+
return unconfirmed;
|
|
1533
|
+
}
|
|
1018
1534
|
}
|
|
1019
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
1020
|
-
if (!
|
|
1535
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1536
|
+
if (!live())
|
|
1021
1537
|
return unconfirmed;
|
|
1022
1538
|
if (bootstrapped.kind !== 'staged')
|
|
1023
1539
|
return bootstrapped;
|
|
1024
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
1540
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1025
1541
|
return null;
|
|
1026
1542
|
}
|
|
1027
1543
|
async function withhold(withheld, room, local, progress) {
|
|
1028
|
-
|
|
1544
|
+
if (withheld.kind === 'unconfirmed')
|
|
1545
|
+
return;
|
|
1546
|
+
if (withheld.kind === 'refused')
|
|
1547
|
+
stale.delete(room.locator);
|
|
1548
|
+
else
|
|
1549
|
+
stale.add(room.locator);
|
|
1550
|
+
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
1551
|
+
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
1552
|
+
repairable.set(room.locator, reset);
|
|
1553
|
+
repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
|
|
1554
|
+
}
|
|
1555
|
+
else
|
|
1556
|
+
repairable.delete(room.locator);
|
|
1029
1557
|
if (withheld.kind === 'scoped-refusal') {
|
|
1030
1558
|
await scoped(withheld, room, local);
|
|
1031
1559
|
}
|
|
@@ -1050,6 +1578,7 @@ export function composeRuntime(options) {
|
|
|
1050
1578
|
function refusedSource(room, { refusal }) {
|
|
1051
1579
|
sourceRefusals.set(room.locator, {
|
|
1052
1580
|
code: refusal.rule,
|
|
1581
|
+
retryable: false,
|
|
1053
1582
|
fields: {
|
|
1054
1583
|
file: refusal.file,
|
|
1055
1584
|
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
@@ -1063,34 +1592,16 @@ export function composeRuntime(options) {
|
|
|
1063
1592
|
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1064
1593
|
}
|
|
1065
1594
|
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
|
-
}
|
|
1595
|
+
if (result.kind === 'expired')
|
|
1596
|
+
return { batch, returned: returnedBy('expired') };
|
|
1082
1597
|
return {
|
|
1083
1598
|
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 }),
|
|
1599
|
+
returned: {
|
|
1600
|
+
kind: result.kind,
|
|
1601
|
+
code: result.code,
|
|
1602
|
+
fields: result.fields,
|
|
1603
|
+
retryable: result.retryable,
|
|
1604
|
+
},
|
|
1094
1605
|
};
|
|
1095
1606
|
}
|
|
1096
1607
|
function logAccepted(locator, submitted, accepted) {
|
|
@@ -1098,84 +1609,56 @@ export function composeRuntime(options) {
|
|
|
1098
1609
|
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
1099
1610
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1100
1611
|
}
|
|
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) {
|
|
1612
|
+
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1613
|
+
await referenced(room.locator);
|
|
1123
1614
|
if (!continuing())
|
|
1124
1615
|
return unconfirmed;
|
|
1125
1616
|
const current = replicas.get(room.locator);
|
|
1126
1617
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1127
1618
|
return null;
|
|
1128
1619
|
}
|
|
1129
|
-
const
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1620
|
+
const held = current.confirmed.record.roomVersion;
|
|
1621
|
+
logAccepted(room.locator, submitted, accepted);
|
|
1622
|
+
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1623
|
+
? { kind: 'staged', confirmed: current.confirmed }
|
|
1624
|
+
: applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1625
|
+
? accepted.roomVersion
|
|
1626
|
+
: held, accepted.roomVersion);
|
|
1627
|
+
if (applied.kind !== 'staged') {
|
|
1628
|
+
const state = pendings.get(submitted.id);
|
|
1629
|
+
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1630
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
1631
|
+
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1632
|
+
emitQueue(room.locator);
|
|
1139
1633
|
return applied;
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
});
|
|
1634
|
+
}
|
|
1635
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1636
|
+
pendings.delete(submitted.id);
|
|
1144
1637
|
unreadable.delete(room.locator);
|
|
1145
1638
|
sourceRefusals.delete(room.locator);
|
|
1146
1639
|
declarationRefusals.delete(room.locator);
|
|
1640
|
+
const set = settled();
|
|
1147
1641
|
list(room, true);
|
|
1148
1642
|
publishChangeSet(set);
|
|
1149
1643
|
moveTo({ ...status, ...attention() });
|
|
1150
1644
|
return null;
|
|
1151
1645
|
}
|
|
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));
|
|
1646
|
+
async function adopted(room, local, submitted, accepted) {
|
|
1647
|
+
const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
|
|
1163
1648
|
if (failed === null)
|
|
1164
1649
|
return true;
|
|
1165
1650
|
if (failed.kind === 'refused' && continuing())
|
|
1166
1651
|
refusedSource(room, failed);
|
|
1167
1652
|
return false;
|
|
1168
1653
|
}
|
|
1169
|
-
async function submitRoom(room, local, authorized, progress) {
|
|
1170
|
-
while (continuing() && authorization !== undefined) {
|
|
1654
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
1655
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1171
1656
|
const replica = replicas.get(room.locator);
|
|
1172
1657
|
const head = replica?.queue[0];
|
|
1173
1658
|
if (replica === undefined || head === undefined)
|
|
1174
|
-
return;
|
|
1175
|
-
if (
|
|
1176
|
-
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1659
|
+
return 'done';
|
|
1660
|
+
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1661
|
+
return 'done';
|
|
1179
1662
|
const state = pendings.get(head.id);
|
|
1180
1663
|
const { submission, lost: earlier } = state ?? {
|
|
1181
1664
|
submission: { state: 'waiting' },
|
|
@@ -1183,26 +1666,33 @@ export function composeRuntime(options) {
|
|
|
1183
1666
|
};
|
|
1184
1667
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1185
1668
|
progress.complete = false;
|
|
1186
|
-
return;
|
|
1669
|
+
return { at: Date.parse(submission.reset) };
|
|
1187
1670
|
}
|
|
1188
1671
|
if (writesHeld()) {
|
|
1189
1672
|
progress.complete = false;
|
|
1190
1673
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1191
|
-
return;
|
|
1674
|
+
return 'done';
|
|
1192
1675
|
}
|
|
1193
1676
|
const lost = earlier || submission.state === 'submitted';
|
|
1677
|
+
if (lost && pastHorizon(head.id)) {
|
|
1678
|
+
await discardPending(room, local, signal);
|
|
1679
|
+
return 'done';
|
|
1680
|
+
}
|
|
1194
1681
|
let result;
|
|
1195
1682
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1196
1683
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1197
|
-
if (discoveryPaused)
|
|
1198
|
-
return;
|
|
1199
|
-
if (writesHeld()) {
|
|
1684
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
1685
|
+
return 'done';
|
|
1686
|
+
if (signal.aborted || writesHeld()) {
|
|
1200
1687
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
1688
|
+
if (signal.aborted)
|
|
1689
|
+
return 'done';
|
|
1201
1690
|
progress.complete = false;
|
|
1202
1691
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1203
|
-
return;
|
|
1692
|
+
return 'done';
|
|
1204
1693
|
}
|
|
1205
1694
|
const opensBefore = openSignals;
|
|
1695
|
+
const answered = awaitingAnswer(room.locator);
|
|
1206
1696
|
try {
|
|
1207
1697
|
result = await options.adapter.mutate(authorized, {
|
|
1208
1698
|
locator: room.locator,
|
|
@@ -1211,33 +1701,40 @@ export function composeRuntime(options) {
|
|
|
1211
1701
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
1212
1702
|
}
|
|
1213
1703
|
catch {
|
|
1704
|
+
answered();
|
|
1214
1705
|
progress.complete = false;
|
|
1215
1706
|
if (continuing())
|
|
1216
1707
|
progress.reachable = false;
|
|
1217
|
-
return;
|
|
1708
|
+
return 'again';
|
|
1709
|
+
}
|
|
1710
|
+
if (signal.aborted) {
|
|
1711
|
+
answered();
|
|
1712
|
+
return 'done';
|
|
1218
1713
|
}
|
|
1714
|
+
if (result.kind !== 'accepted' || !continuing())
|
|
1715
|
+
answered();
|
|
1219
1716
|
if (!continuing())
|
|
1220
|
-
return;
|
|
1717
|
+
return 'done';
|
|
1221
1718
|
if (result.kind === 'transport-uncertainty') {
|
|
1222
1719
|
progress.complete = false;
|
|
1223
1720
|
progress.reachable = false;
|
|
1224
|
-
return;
|
|
1721
|
+
return 'again';
|
|
1225
1722
|
}
|
|
1226
1723
|
if (result.kind === 'room-not-readable') {
|
|
1227
1724
|
progress.complete = false;
|
|
1228
1725
|
unreadable.add(room.locator);
|
|
1229
1726
|
list(room, false);
|
|
1230
1727
|
emitRooms();
|
|
1231
|
-
return;
|
|
1728
|
+
return 'done';
|
|
1232
1729
|
}
|
|
1233
1730
|
if (result.kind === 'revoked-authorization') {
|
|
1234
1731
|
await refuse('revoked-authorization');
|
|
1235
|
-
return;
|
|
1732
|
+
return 'done';
|
|
1236
1733
|
}
|
|
1237
1734
|
if (result.kind === 'scoped-refusal') {
|
|
1238
1735
|
await scoped(result, room, local);
|
|
1239
1736
|
progress.complete = false;
|
|
1240
|
-
return;
|
|
1737
|
+
return 'done';
|
|
1241
1738
|
}
|
|
1242
1739
|
if (result.kind === 'write-halted') {
|
|
1243
1740
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
@@ -1246,16 +1743,24 @@ export function composeRuntime(options) {
|
|
|
1246
1743
|
continue;
|
|
1247
1744
|
haltedRefusal = true;
|
|
1248
1745
|
progress.complete = false;
|
|
1249
|
-
return;
|
|
1746
|
+
return 'done';
|
|
1250
1747
|
}
|
|
1251
1748
|
if (result.kind === 'conflicted' ||
|
|
1252
1749
|
result.kind === 'rejected' ||
|
|
1253
1750
|
result.kind === 'expired' ||
|
|
1254
1751
|
result.kind === 'retryable-refusal') {
|
|
1752
|
+
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
1753
|
+
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
1754
|
+
await discardPending(room, local, signal);
|
|
1755
|
+
return 'done';
|
|
1756
|
+
}
|
|
1255
1757
|
if (result.kind === 'expired') {
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1758
|
+
await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
|
|
1759
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
1760
|
+
if (withheld !== null) {
|
|
1761
|
+
await withhold(withheld, room, local, progress);
|
|
1762
|
+
return 'done';
|
|
1763
|
+
}
|
|
1259
1764
|
}
|
|
1260
1765
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1261
1766
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1268,7 +1773,7 @@ export function composeRuntime(options) {
|
|
|
1268
1773
|
const reset = result.reset;
|
|
1269
1774
|
progress.complete = false;
|
|
1270
1775
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1271
|
-
return;
|
|
1776
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1272
1777
|
}
|
|
1273
1778
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1274
1779
|
const left = leaving(head, result);
|
|
@@ -1279,113 +1784,96 @@ export function composeRuntime(options) {
|
|
|
1279
1784
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1280
1785
|
return;
|
|
1281
1786
|
}
|
|
1282
|
-
const
|
|
1283
|
-
leaving: result.kind === 'expired' && landed(head, current.confirmed)
|
|
1787
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1788
|
+
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1789
|
+
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1284
1790
|
? { ...left, returned: null }
|
|
1285
1791
|
: left,
|
|
1286
1792
|
});
|
|
1287
|
-
publishChangeSet(
|
|
1793
|
+
publishChangeSet(settled());
|
|
1288
1794
|
moveTo({ ...status, ...attention() });
|
|
1289
1795
|
});
|
|
1290
1796
|
if (result.kind === 'expired')
|
|
1291
1797
|
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;
|
|
1798
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
1799
|
+
if (withheld !== null) {
|
|
1800
|
+
await withhold(withheld, room, local, progress);
|
|
1801
|
+
return 'done';
|
|
1318
1802
|
}
|
|
1319
1803
|
continue;
|
|
1320
1804
|
}
|
|
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
|
-
}
|
|
1805
|
+
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1806
|
+
if (!kept) {
|
|
1341
1807
|
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;
|
|
1354
|
-
}
|
|
1355
|
-
progress.complete = false;
|
|
1356
|
-
if (pulled.kind === 'transport-uncertainty') {
|
|
1357
|
-
progress.reachable = false;
|
|
1808
|
+
return 'done';
|
|
1358
1809
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1810
|
+
}
|
|
1811
|
+
return 'done';
|
|
1812
|
+
}
|
|
1813
|
+
function pastHorizon(identity) {
|
|
1814
|
+
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
1815
|
+
}
|
|
1816
|
+
async function discardPending(room, local, signal) {
|
|
1817
|
+
const { locator } = room;
|
|
1818
|
+
const discarded = await exclusive(async () => {
|
|
1819
|
+
if (fences.has(locator))
|
|
1820
|
+
return false;
|
|
1821
|
+
const before = replicas.get(locator);
|
|
1822
|
+
const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
1823
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1824
|
+
lastFenced.set(locator, ++fencings);
|
|
1825
|
+
try {
|
|
1826
|
+
await local.discard(locator);
|
|
1366
1827
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1828
|
+
finally {
|
|
1829
|
+
fences.delete(locator);
|
|
1369
1830
|
}
|
|
1831
|
+
for (const batch of queue)
|
|
1832
|
+
pendings.delete(batch.id);
|
|
1833
|
+
queuesWithoutReplica.delete(locator);
|
|
1834
|
+
const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
|
|
1835
|
+
if (after !== null)
|
|
1836
|
+
replicas.set(locator, after);
|
|
1837
|
+
journal.record(`room discarded ${locator}`);
|
|
1838
|
+
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
1839
|
+
moveTo({ ...status, ...attention() });
|
|
1840
|
+
return true;
|
|
1841
|
+
});
|
|
1842
|
+
if (!discarded)
|
|
1370
1843
|
return;
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1844
|
+
const progress = { complete: true, reachable: true };
|
|
1845
|
+
await roomTask(locator, async (live) => {
|
|
1846
|
+
await Promise.race([
|
|
1847
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
1848
|
+
abandonedOn(signal),
|
|
1849
|
+
]);
|
|
1850
|
+
});
|
|
1851
|
+
if (signal.aborted)
|
|
1852
|
+
return;
|
|
1853
|
+
await rest(progress);
|
|
1854
|
+
}
|
|
1855
|
+
function awaitingAnswer(locator) {
|
|
1856
|
+
let release = () => { };
|
|
1857
|
+
const awaited = new Promise((resolve) => {
|
|
1858
|
+
release = resolve;
|
|
1859
|
+
});
|
|
1860
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1861
|
+
return () => {
|
|
1862
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1863
|
+
answering.delete(locator);
|
|
1864
|
+
release();
|
|
1865
|
+
};
|
|
1373
1866
|
}
|
|
1374
1867
|
async function retainModel(local, room, attached, next) {
|
|
1375
1868
|
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
1869
|
const previous = modelOf(room.locator, attached);
|
|
1380
1870
|
const heldBefore = replicas.get(room.locator)?.confirmed.record;
|
|
1381
1871
|
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
1382
1872
|
(heldBefore === undefined ||
|
|
1383
1873
|
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
1384
1874
|
JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
|
|
1385
|
-
JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
|
|
1386
1875
|
JSON.stringify(heldBefore.typedSelection) ===
|
|
1387
|
-
JSON.stringify(room.typedSelection ?? null)
|
|
1388
|
-
JSON.stringify(heldBefore.untypedFolders) === JSON.stringify(room.untypedFolders ?? []))))
|
|
1876
|
+
JSON.stringify(room.typedSelection ?? null))))
|
|
1389
1877
|
return;
|
|
1390
1878
|
if (next.status === 'unavailable') {
|
|
1391
1879
|
invalidModels.add(key);
|
|
@@ -1394,37 +1882,30 @@ export function composeRuntime(options) {
|
|
|
1394
1882
|
await exclusive(async () => {
|
|
1395
1883
|
if (!continuing())
|
|
1396
1884
|
return;
|
|
1397
|
-
if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
|
|
1398
|
-
return;
|
|
1399
1885
|
const held = replicas.get(room.locator);
|
|
1400
1886
|
if (held !== undefined) {
|
|
1401
1887
|
const declared = {
|
|
1402
1888
|
...held.confirmed.record,
|
|
1403
1889
|
operations: room.operations,
|
|
1404
1890
|
replica: room.replica,
|
|
1405
|
-
attachedModel: room.attachedModel,
|
|
1406
1891
|
typedSelection: room.typedSelection ?? null,
|
|
1407
|
-
untypedFolders: room.untypedFolders ?? [],
|
|
1408
1892
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1409
1893
|
};
|
|
1410
1894
|
const reclassified = sameMembership(declared, held.confirmed.record)
|
|
1411
|
-
?
|
|
1895
|
+
? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
|
|
1412
1896
|
: readReclassified(declared, held.confirmed);
|
|
1413
1897
|
const record = reclassified.kind === 'staged'
|
|
1414
1898
|
? declared
|
|
1415
1899
|
: {
|
|
1416
1900
|
...declared,
|
|
1417
1901
|
typedSelection: held.confirmed.record.typedSelection,
|
|
1418
|
-
untypedFolders: held.confirmed.record.untypedFolders,
|
|
1419
1902
|
};
|
|
1420
|
-
const read = reclassified.kind === 'deferred'
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
queue: held.queue,
|
|
1425
|
-
});
|
|
1903
|
+
const read = reclassified.kind === 'deferred'
|
|
1904
|
+
? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
|
|
1905
|
+
: reclassified;
|
|
1906
|
+
await local.write({ next: record, previous: held.confirmed.record });
|
|
1426
1907
|
if (read.kind === 'staged') {
|
|
1427
|
-
replicas.set(room.locator, replicaOf(read.confirmed, held.queue,
|
|
1908
|
+
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1428
1909
|
if (declarationRefusals.delete(room.locator)) {
|
|
1429
1910
|
sourceRefusals.delete(room.locator);
|
|
1430
1911
|
unreadable.delete(room.locator);
|
|
@@ -1448,79 +1929,12 @@ export function composeRuntime(options) {
|
|
|
1448
1929
|
publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
|
|
1449
1930
|
});
|
|
1450
1931
|
}
|
|
1451
|
-
async function acquireModel(local, room, authorized, selection) {
|
|
1452
|
-
const attached = selection === null ? 'collection' : 'typed-selection';
|
|
1453
|
-
const key = modelKey(room.locator, attached);
|
|
1454
|
-
const declared = selection === null ? room.attachedModel : selection.attachedModel;
|
|
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' });
|
|
1506
|
-
return;
|
|
1507
|
-
}
|
|
1508
|
-
const verified = answer.snapshot.schema === null ? EMPTY_SCHEMA : verify(answer.snapshot.schema);
|
|
1509
|
-
if (verified.verdict === 'refuse') {
|
|
1510
|
-
const where = verified.location === undefined ? '' : ` ${verified.location}`;
|
|
1511
|
-
journal.record(`model invalid ${room.locator} ${attached} ${answer.snapshot.identity} ${verified.rule}${where}`);
|
|
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
1932
|
async function scoped(result, room, local) {
|
|
1523
|
-
scopedRefusals.set(room.locator, {
|
|
1933
|
+
scopedRefusals.set(room.locator, {
|
|
1934
|
+
code: result.code,
|
|
1935
|
+
fields: result.fields,
|
|
1936
|
+
retryable: result.retryable,
|
|
1937
|
+
});
|
|
1524
1938
|
let scopedAway = room;
|
|
1525
1939
|
if (result.scope === 'model')
|
|
1526
1940
|
await retainModel(local, room, 'collection', {
|
|
@@ -1546,14 +1960,38 @@ export function composeRuntime(options) {
|
|
|
1546
1960
|
emitRooms();
|
|
1547
1961
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
1548
1962
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1963
|
+
function heldRooms(listed) {
|
|
1964
|
+
return listed.flatMap((room) => {
|
|
1965
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1966
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1967
|
+
return [];
|
|
1968
|
+
return [
|
|
1969
|
+
{
|
|
1970
|
+
locator: room.locator,
|
|
1971
|
+
roomVersion: record.roomVersion,
|
|
1972
|
+
documents: record.documents,
|
|
1973
|
+
revisions: record.revisions,
|
|
1974
|
+
},
|
|
1975
|
+
];
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
1979
|
+
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
1980
|
+
if (!live())
|
|
1981
|
+
return;
|
|
1982
|
+
const ready = listed.filter((_, index) => {
|
|
1552
1983
|
const outcome = prepared[index];
|
|
1553
1984
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1554
1985
|
});
|
|
1555
|
-
|
|
1556
|
-
|
|
1986
|
+
if (attempt === 'opened') {
|
|
1987
|
+
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
1988
|
+
handed = true;
|
|
1989
|
+
}
|
|
1990
|
+
discovering?.release();
|
|
1991
|
+
const loaded = await Promise.allSettled(ready
|
|
1992
|
+
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1993
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1994
|
+
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1557
1995
|
if (failed !== undefined)
|
|
1558
1996
|
throw failed.reason;
|
|
1559
1997
|
}
|
|
@@ -1570,11 +2008,8 @@ export function composeRuntime(options) {
|
|
|
1570
2008
|
progress.complete = false;
|
|
1571
2009
|
return false;
|
|
1572
2010
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
acquireModel(local, room, authorization, null),
|
|
1576
|
-
...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
|
|
1577
|
-
]);
|
|
2011
|
+
if (prior !== undefined)
|
|
2012
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1578
2013
|
if (discoveryPaused || authorization === undefined)
|
|
1579
2014
|
return false;
|
|
1580
2015
|
if (room.replica.admission !== 'admitted') {
|
|
@@ -1582,29 +2017,65 @@ export function composeRuntime(options) {
|
|
|
1582
2017
|
progress.complete = false;
|
|
1583
2018
|
return false;
|
|
1584
2019
|
}
|
|
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
2020
|
return true;
|
|
1591
2021
|
}
|
|
1592
|
-
async function
|
|
1593
|
-
|
|
2022
|
+
async function caughtUp(room, local, progress, live) {
|
|
2023
|
+
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
2024
|
+
if (held === undefined || authorization === undefined || !live())
|
|
2025
|
+
return;
|
|
2026
|
+
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
2027
|
+
if (!behind)
|
|
2028
|
+
return;
|
|
2029
|
+
const withheld = await advance(room, local, authorization, live);
|
|
2030
|
+
if (withheld !== null)
|
|
2031
|
+
await withhold(withheld, room, local, progress);
|
|
2032
|
+
}
|
|
2033
|
+
function revoked(locator) {
|
|
2034
|
+
return ['collection', 'typed-selection'].some((attached) => {
|
|
2035
|
+
const model = modelOf(locator, attached);
|
|
2036
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
async function loadRoom(room, local, progress, live) {
|
|
2040
|
+
if (!live() || authorization === undefined)
|
|
2041
|
+
return;
|
|
2042
|
+
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
2043
|
+
await reload(room, local, progress, live);
|
|
1594
2044
|
return;
|
|
1595
|
-
|
|
2045
|
+
}
|
|
2046
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1596
2047
|
if (withheld !== null) {
|
|
1597
2048
|
await withhold(withheld, room, local, progress);
|
|
1598
2049
|
return;
|
|
1599
2050
|
}
|
|
1600
|
-
await
|
|
2051
|
+
await caughtUp(room, local, progress, live);
|
|
2052
|
+
}
|
|
2053
|
+
async function reload(room, local, progress, live) {
|
|
2054
|
+
try {
|
|
2055
|
+
if (!live() || authorization === undefined)
|
|
2056
|
+
return;
|
|
2057
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2058
|
+
if (!live())
|
|
2059
|
+
return;
|
|
2060
|
+
if (bootstrapped.kind !== 'staged') {
|
|
2061
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
2065
|
+
await caughtUp(room, local, progress, live);
|
|
2066
|
+
}
|
|
2067
|
+
finally {
|
|
2068
|
+
caughtUpTo(room.locator);
|
|
2069
|
+
}
|
|
1601
2070
|
}
|
|
1602
|
-
async function
|
|
2071
|
+
async function discoverRooms(local, progress, attempt, live) {
|
|
1603
2072
|
if (closed || authorization === undefined) {
|
|
1604
2073
|
notifyReady();
|
|
1605
2074
|
return;
|
|
1606
2075
|
}
|
|
1607
2076
|
moveTo(transferring(status));
|
|
2077
|
+
discoveries += 1;
|
|
2078
|
+
const started = discoveries;
|
|
1608
2079
|
let result;
|
|
1609
2080
|
try {
|
|
1610
2081
|
result = await options.adapter.discover(authorization);
|
|
@@ -1612,7 +2083,8 @@ export function composeRuntime(options) {
|
|
|
1612
2083
|
catch {
|
|
1613
2084
|
result = { kind: 'transport-uncertainty' };
|
|
1614
2085
|
}
|
|
1615
|
-
if (
|
|
2086
|
+
if (!live()) {
|
|
2087
|
+
progress.complete = false;
|
|
1616
2088
|
notifyReady();
|
|
1617
2089
|
return;
|
|
1618
2090
|
}
|
|
@@ -1625,12 +2097,17 @@ export function composeRuntime(options) {
|
|
|
1625
2097
|
}
|
|
1626
2098
|
}
|
|
1627
2099
|
progress.reachable = false;
|
|
2100
|
+
progress.complete = false;
|
|
1628
2101
|
notifyReady();
|
|
1629
2102
|
return;
|
|
1630
2103
|
}
|
|
1631
2104
|
if (result.kind === 'contract-unavailable') {
|
|
1632
2105
|
journal.record(`connection contract unavailable ${result.code}`);
|
|
1633
|
-
await refuse('missing-required-guarantee', {
|
|
2106
|
+
await refuse('missing-required-guarantee', {
|
|
2107
|
+
code: result.code,
|
|
2108
|
+
fields: result.fields,
|
|
2109
|
+
retryable: result.retryable,
|
|
2110
|
+
});
|
|
1634
2111
|
notifyReady();
|
|
1635
2112
|
return;
|
|
1636
2113
|
}
|
|
@@ -1639,17 +2116,6 @@ export function composeRuntime(options) {
|
|
|
1639
2116
|
notifyReady();
|
|
1640
2117
|
return;
|
|
1641
2118
|
}
|
|
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
2119
|
for (const room of result.rooms) {
|
|
1654
2120
|
if (room.operations === undefined ||
|
|
1655
2121
|
!['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
@@ -1657,8 +2123,7 @@ export function composeRuntime(options) {
|
|
|
1657
2123
|
room.replica === undefined ||
|
|
1658
2124
|
typeof room.replica.selection !== 'string' ||
|
|
1659
2125
|
room.replica.selection.length === 0 ||
|
|
1660
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
1661
|
-
!declaredIdentity(room.attachedModel)) {
|
|
2126
|
+
!['admitted', 'unavailable'].includes(room.replica.admission)) {
|
|
1662
2127
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
1663
2128
|
await refuse('missing-required-guarantee');
|
|
1664
2129
|
notifyReady();
|
|
@@ -1670,20 +2135,6 @@ export function composeRuntime(options) {
|
|
|
1670
2135
|
notifyReady();
|
|
1671
2136
|
return;
|
|
1672
2137
|
}
|
|
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
2138
|
}
|
|
1688
2139
|
if (refusal !== null) {
|
|
1689
2140
|
refusal = null;
|
|
@@ -1691,30 +2142,20 @@ export function composeRuntime(options) {
|
|
|
1691
2142
|
}
|
|
1692
2143
|
const previousCapabilities = capabilities;
|
|
1693
2144
|
capabilities = result.capabilities;
|
|
2145
|
+
outcomeRetention = result.retention.outcome;
|
|
1694
2146
|
notifyReady();
|
|
1695
|
-
if (declared.has('room-version-notifications'))
|
|
1696
|
-
observeRoomVersions();
|
|
1697
|
-
const discoveredSlots = result.slots ?? [];
|
|
1698
2147
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1699
|
-
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)
|
|
1700
|
-
JSON.stringify(discoveredSlots) !== JSON.stringify(slots)) {
|
|
2148
|
+
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
1701
2149
|
limits = result.providerLimits;
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
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);
|
|
2150
|
+
await local.declare(limits, capabilities);
|
|
2151
|
+
}
|
|
2152
|
+
const signedIn = result.member ?? null;
|
|
2153
|
+
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
2154
|
+
await local.keepMember(signedIn);
|
|
2155
|
+
member = signedIn;
|
|
2156
|
+
emitMember();
|
|
2157
|
+
}
|
|
2158
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
1718
2159
|
}
|
|
1719
2160
|
async function ensureAuthorization() {
|
|
1720
2161
|
if (authorization !== undefined)
|
|
@@ -1728,66 +2169,413 @@ export function composeRuntime(options) {
|
|
|
1728
2169
|
throw error;
|
|
1729
2170
|
}
|
|
1730
2171
|
}
|
|
1731
|
-
async function
|
|
1732
|
-
if (
|
|
1733
|
-
|
|
1734
|
-
return;
|
|
2172
|
+
async function admittedRead(work) {
|
|
2173
|
+
if (roomReads.generation !== generation) {
|
|
2174
|
+
roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
1735
2175
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
2176
|
+
const reads = roomReads;
|
|
2177
|
+
while (reads.free === 0)
|
|
2178
|
+
await new Promise((admit) => reads.waiting.push(admit));
|
|
2179
|
+
reads.free -= 1;
|
|
1739
2180
|
try {
|
|
1740
|
-
|
|
1741
|
-
|
|
2181
|
+
await work();
|
|
2182
|
+
}
|
|
2183
|
+
finally {
|
|
2184
|
+
reads.free += 1;
|
|
2185
|
+
reads.waiting.shift()?.();
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
function roomTask(locator, work) {
|
|
2189
|
+
return task(roomLane(locator), (live) => admittedRead(() => work(live)));
|
|
2190
|
+
}
|
|
2191
|
+
function task(lane, work) {
|
|
2192
|
+
if (stopping)
|
|
2193
|
+
return Promise.resolve();
|
|
2194
|
+
const held = lanes.get(lane);
|
|
2195
|
+
const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
|
|
2196
|
+
const turn = current.generation;
|
|
2197
|
+
const live = () => turn === generation && continuing();
|
|
2198
|
+
const run = current.chain.then(() => work(live)).catch(() => undefined);
|
|
2199
|
+
current.chain = run;
|
|
2200
|
+
lanes.set(lane, current);
|
|
2201
|
+
working.add(run);
|
|
2202
|
+
void run.finally(() => working.delete(run));
|
|
2203
|
+
return run;
|
|
2204
|
+
}
|
|
2205
|
+
function sync(attempt) {
|
|
2206
|
+
return task(OPENING_LANE, async (live) => {
|
|
2207
|
+
if (!live()) {
|
|
2208
|
+
notifyReady();
|
|
1742
2209
|
return;
|
|
1743
|
-
|
|
1744
|
-
|
|
2210
|
+
}
|
|
2211
|
+
const progress = { complete: true, reachable: true };
|
|
2212
|
+
const gate = discover();
|
|
2213
|
+
if (attempt === 'opened')
|
|
2214
|
+
opened = generation;
|
|
2215
|
+
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
2216
|
+
if (attempt === 'opened')
|
|
2217
|
+
streamed = progress;
|
|
2218
|
+
complete = false;
|
|
2219
|
+
recorded = false;
|
|
2220
|
+
moveTo(transferring(status));
|
|
2221
|
+
handed = false;
|
|
2222
|
+
try {
|
|
2223
|
+
const local = await openLocal();
|
|
2224
|
+
if (!live())
|
|
2225
|
+
return;
|
|
2226
|
+
await ensureAuthorization();
|
|
2227
|
+
if (!live())
|
|
2228
|
+
return;
|
|
2229
|
+
await discoverRooms(local, progress, attempt, live);
|
|
2230
|
+
}
|
|
2231
|
+
catch {
|
|
2232
|
+
progress.complete = false;
|
|
2233
|
+
if (attempt === 'opened' && !handed)
|
|
2234
|
+
progress.reachable = false;
|
|
2235
|
+
}
|
|
2236
|
+
finally {
|
|
2237
|
+
gate.release();
|
|
2238
|
+
notifyReady();
|
|
2239
|
+
}
|
|
2240
|
+
if (!live())
|
|
1745
2241
|
return;
|
|
1746
|
-
|
|
1747
|
-
|
|
2242
|
+
if (attempt !== 'opened') {
|
|
2243
|
+
await finishOpening(progress, live);
|
|
1748
2244
|
return;
|
|
1749
|
-
if (progress.complete) {
|
|
1750
|
-
completedAt = new Date().toISOString();
|
|
1751
|
-
await local.complete(completedAt);
|
|
1752
2245
|
}
|
|
2246
|
+
if (handed || closed || progress.reachable)
|
|
2247
|
+
return;
|
|
2248
|
+
underway = null;
|
|
2249
|
+
opened = null;
|
|
2250
|
+
journal.record('connection reopen');
|
|
2251
|
+
await rest(progress, live);
|
|
2252
|
+
options.adapter.roomStream?.reopen();
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
function closedSocket(revoked) {
|
|
2256
|
+
const opened = socketOpen;
|
|
2257
|
+
socketOpen = false;
|
|
2258
|
+
if (revoked) {
|
|
2259
|
+
void refuse('revoked-authorization');
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
complete = false;
|
|
2263
|
+
if (streamed !== null)
|
|
2264
|
+
streamed.complete = false;
|
|
2265
|
+
moveTo({
|
|
2266
|
+
...status,
|
|
2267
|
+
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
2268
|
+
providerUnreachable: status.providerUnreachable || !opened,
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
function applyOpening(expired) {
|
|
2272
|
+
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2273
|
+
const cycle = progress ?? { complete: true, reachable: true };
|
|
2274
|
+
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2275
|
+
if (opened === generation)
|
|
2276
|
+
opened = null;
|
|
2277
|
+
return task(OPENING_LANE, async (live) => {
|
|
2278
|
+
underway = null;
|
|
2279
|
+
if (!live())
|
|
2280
|
+
return;
|
|
2281
|
+
if (progress === null) {
|
|
2282
|
+
await replacing;
|
|
2283
|
+
await rest(cycle, live);
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
await roomsCarried(expired);
|
|
2287
|
+
if (!live())
|
|
2288
|
+
return;
|
|
2289
|
+
const replaced = new Set(owed);
|
|
2290
|
+
owed.clear();
|
|
2291
|
+
try {
|
|
2292
|
+
const local = await openLocal();
|
|
2293
|
+
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
2294
|
+
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2295
|
+
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2296
|
+
for (const room of reloading)
|
|
2297
|
+
catching.add(room.locator);
|
|
2298
|
+
readSide.recover();
|
|
2299
|
+
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2300
|
+
const reloaded = [...(await reloads), ...(await replacing)];
|
|
2301
|
+
if (progress.reachable) {
|
|
2302
|
+
for (const room of held) {
|
|
2303
|
+
if (!expired.includes(room.locator) && !reloading.includes(room)) {
|
|
2304
|
+
await confirmModels(room, local);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
2309
|
+
if (failed !== undefined)
|
|
2310
|
+
throw failed.reason;
|
|
2311
|
+
}
|
|
2312
|
+
catch {
|
|
2313
|
+
progress.complete = false;
|
|
2314
|
+
progress.reachable = false;
|
|
2315
|
+
}
|
|
2316
|
+
if (live())
|
|
2317
|
+
await finishOpening(progress, live);
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
async function reloadNow(replaced, progress) {
|
|
2321
|
+
try {
|
|
2322
|
+
const local = await openLocal();
|
|
2323
|
+
return await Promise.allSettled([...discovered.values()]
|
|
2324
|
+
.filter((room) => replicas.has(room.locator) && replaced(room))
|
|
2325
|
+
.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
1753
2326
|
}
|
|
1754
2327
|
catch {
|
|
1755
|
-
progress.
|
|
1756
|
-
|
|
2328
|
+
progress.reachable = false;
|
|
2329
|
+
return [];
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
async function confirmModels(room, local) {
|
|
2333
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
2334
|
+
const model = modelOf(room.locator, attached);
|
|
2335
|
+
if (model.status === 'cached') {
|
|
2336
|
+
await retainModel(local, room, attached, { ...model, status: 'current' });
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
function submitLater(locator) {
|
|
2341
|
+
if (localSaves)
|
|
1757
2342
|
return;
|
|
2343
|
+
void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
|
|
2344
|
+
}
|
|
2345
|
+
async function submitPass(locator, signal) {
|
|
2346
|
+
const room = discovered.get(locator);
|
|
2347
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
2348
|
+
return 'done';
|
|
2349
|
+
const progress = { complete: true, reachable: true };
|
|
2350
|
+
let next;
|
|
2351
|
+
try {
|
|
2352
|
+
await ensureAuthorization();
|
|
2353
|
+
if (authorization === undefined)
|
|
2354
|
+
return 'done';
|
|
2355
|
+
next = await submitRoom(room, await openLocal(), authorization, progress, signal);
|
|
2356
|
+
}
|
|
2357
|
+
catch {
|
|
2358
|
+
progress.reachable = false;
|
|
2359
|
+
next = 'again';
|
|
2360
|
+
}
|
|
2361
|
+
if (signal.aborted)
|
|
2362
|
+
return 'done';
|
|
2363
|
+
await rest(progress);
|
|
2364
|
+
return next === 'again' && !socketOpen ? 'done' : next;
|
|
2365
|
+
}
|
|
2366
|
+
function repairLater(locator, when) {
|
|
2367
|
+
if (!socketOpen || repairing.has(locator))
|
|
2368
|
+
return;
|
|
2369
|
+
void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
|
|
2370
|
+
}
|
|
2371
|
+
async function repairPass(locator, signal) {
|
|
2372
|
+
const room = discovered.get(locator);
|
|
2373
|
+
if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
|
|
2374
|
+
return 'done';
|
|
2375
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2376
|
+
return 'done';
|
|
2377
|
+
const progress = { complete: true, reachable: true };
|
|
2378
|
+
repairing.add(locator);
|
|
2379
|
+
try {
|
|
2380
|
+
const local = await openLocal();
|
|
2381
|
+
await roomTask(locator, async (live) => {
|
|
2382
|
+
const current = () => live() && !signal.aborted;
|
|
2383
|
+
if (!current() || authorization === undefined)
|
|
2384
|
+
return;
|
|
2385
|
+
const withheld = await Promise.race([
|
|
2386
|
+
advance(room, local, authorization, current),
|
|
2387
|
+
abandonedOn(signal),
|
|
2388
|
+
]);
|
|
2389
|
+
if (withheld !== null && current())
|
|
2390
|
+
await withhold(withheld, room, local, progress);
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
catch {
|
|
2394
|
+
stale.add(locator);
|
|
2395
|
+
progress.reachable = false;
|
|
1758
2396
|
}
|
|
1759
2397
|
finally {
|
|
1760
|
-
|
|
1761
|
-
|
|
2398
|
+
repairing.delete(locator);
|
|
2399
|
+
}
|
|
2400
|
+
if (signal.aborted)
|
|
2401
|
+
return 'done';
|
|
2402
|
+
await rest(progress);
|
|
2403
|
+
if (!stale.has(locator)) {
|
|
2404
|
+
if ((replicas.get(locator)?.queue.length ?? 0) > 0)
|
|
2405
|
+
submitLater(locator);
|
|
2406
|
+
return 'done';
|
|
2407
|
+
}
|
|
2408
|
+
if (!repairable.has(locator))
|
|
2409
|
+
return 'done';
|
|
2410
|
+
const reset = repairable.get(locator);
|
|
2411
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2412
|
+
}
|
|
2413
|
+
function resendLater() {
|
|
2414
|
+
if (!socketOpen)
|
|
2415
|
+
return;
|
|
2416
|
+
void recovery.attempt('resend', resendPass, { again: true });
|
|
2417
|
+
}
|
|
2418
|
+
async function resendPass(signal) {
|
|
2419
|
+
if (!socketOpen || closed || stopping || discoveryPaused)
|
|
2420
|
+
return 'done';
|
|
2421
|
+
const reading = { complete: true, reachable: true };
|
|
2422
|
+
try {
|
|
2423
|
+
await conversations.resend(reading);
|
|
2424
|
+
}
|
|
2425
|
+
catch {
|
|
2426
|
+
reading.reachable = false;
|
|
2427
|
+
}
|
|
2428
|
+
if (signal.aborted)
|
|
2429
|
+
return 'done';
|
|
2430
|
+
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
2431
|
+
}
|
|
2432
|
+
function submitAll() {
|
|
2433
|
+
for (const [locator, replica] of replicas) {
|
|
2434
|
+
if (replica.queue.length > 0)
|
|
2435
|
+
submitLater(locator);
|
|
1762
2436
|
}
|
|
1763
2437
|
}
|
|
1764
|
-
async function
|
|
2438
|
+
async function finishOpening(progress, live) {
|
|
1765
2439
|
try {
|
|
1766
|
-
|
|
2440
|
+
if (!live()) {
|
|
2441
|
+
progress.complete = false;
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2444
|
+
submitAll();
|
|
2445
|
+
for (const locator of stale)
|
|
2446
|
+
if (repairable.has(locator))
|
|
2447
|
+
repairLater(locator, {});
|
|
2448
|
+
await conversations.reconcile();
|
|
2449
|
+
if (status.providerUnreachable)
|
|
2450
|
+
conversations.reconnected();
|
|
2451
|
+
const reading = { complete: true, reachable: true };
|
|
2452
|
+
await conversations.catchUp(reading);
|
|
2453
|
+
if (progress.reachable && reading.reachable)
|
|
2454
|
+
await conversations.resend(reading);
|
|
2455
|
+
if (!reading.reachable) {
|
|
2456
|
+
progress.reachable = false;
|
|
2457
|
+
resendLater();
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
catch {
|
|
2461
|
+
progress.complete = false;
|
|
1767
2462
|
}
|
|
1768
2463
|
finally {
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
void enqueue();
|
|
2464
|
+
if (live()) {
|
|
2465
|
+
complete = progress.complete;
|
|
2466
|
+
await rest(progress, live);
|
|
1773
2467
|
}
|
|
1774
2468
|
}
|
|
1775
2469
|
}
|
|
1776
|
-
function
|
|
1777
|
-
|
|
1778
|
-
if (path === undefined || withdrawNotifications !== undefined)
|
|
2470
|
+
async function rest(progress, live = () => !closed) {
|
|
2471
|
+
if (!live())
|
|
1779
2472
|
return;
|
|
1780
|
-
|
|
1781
|
-
|
|
2473
|
+
let lastCompleted = status.lastCompleted;
|
|
2474
|
+
let synced = complete && stale.size === 0 && !pending();
|
|
2475
|
+
if (synced && !recorded) {
|
|
2476
|
+
const completedAt = new Date().toISOString();
|
|
2477
|
+
try {
|
|
2478
|
+
await (await openLocal()).complete(completedAt);
|
|
2479
|
+
if (!live())
|
|
2480
|
+
return;
|
|
2481
|
+
recorded = true;
|
|
2482
|
+
lastCompleted = completedAt;
|
|
2483
|
+
}
|
|
2484
|
+
catch {
|
|
2485
|
+
synced = false;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
moveTo({
|
|
2489
|
+
state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
|
|
2490
|
+
lastCompleted,
|
|
2491
|
+
providerUnreachable: !progress.reachable,
|
|
2492
|
+
...attention(),
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
function discover() {
|
|
2496
|
+
let release = () => { };
|
|
2497
|
+
const done = new Promise((resolve) => {
|
|
2498
|
+
release = resolve;
|
|
2499
|
+
});
|
|
2500
|
+
discovering = { generation, done, release };
|
|
2501
|
+
return discovering;
|
|
2502
|
+
}
|
|
2503
|
+
function applyRoom(locator, roomVersion) {
|
|
2504
|
+
announced.set(locator, roomVersion);
|
|
2505
|
+
return task(roomLane(locator), async (live) => {
|
|
2506
|
+
if (discovering?.generation === generation)
|
|
2507
|
+
await discovering.done;
|
|
2508
|
+
const saving = answering.get(locator);
|
|
2509
|
+
if (saving?.generation === generation)
|
|
2510
|
+
await saving.answered;
|
|
2511
|
+
if (!live() || authorization === undefined)
|
|
1782
2512
|
return;
|
|
1783
|
-
|
|
1784
|
-
|
|
2513
|
+
const room = discovered.get(locator);
|
|
2514
|
+
const held = replicas.get(locator);
|
|
2515
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2516
|
+
return;
|
|
2517
|
+
if (held !== undefined &&
|
|
2518
|
+
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
2519
|
+
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1785
2520
|
return;
|
|
1786
2521
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
2522
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
2523
|
+
const progress = during ?? { complete: true, reachable: true };
|
|
2524
|
+
if (during === null)
|
|
2525
|
+
moveTo(transferring(status));
|
|
2526
|
+
try {
|
|
2527
|
+
const local = await openLocal();
|
|
2528
|
+
await admittedRead(async () => {
|
|
2529
|
+
if (!live() || authorization === undefined)
|
|
2530
|
+
return;
|
|
2531
|
+
const withheld = await advance(room, local, authorization, live);
|
|
2532
|
+
if (withheld !== null)
|
|
2533
|
+
await withhold(withheld, room, local, progress);
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
catch {
|
|
2537
|
+
stale.add(locator);
|
|
2538
|
+
}
|
|
2539
|
+
finally {
|
|
2540
|
+
if (during === null)
|
|
2541
|
+
await rest(progress, live);
|
|
2542
|
+
}
|
|
1789
2543
|
});
|
|
1790
2544
|
}
|
|
2545
|
+
function roomsCarried(replacing) {
|
|
2546
|
+
const replaced = new Set(replacing.map(roomLane));
|
|
2547
|
+
return Promise.all([...lanes]
|
|
2548
|
+
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
2549
|
+
.map(([, held]) => held.chain));
|
|
2550
|
+
}
|
|
2551
|
+
const receipts = {
|
|
2552
|
+
heads: async () => (await openLocal()).receivedHeads(),
|
|
2553
|
+
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
2554
|
+
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
2555
|
+
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
2556
|
+
};
|
|
2557
|
+
function observeStream() {
|
|
2558
|
+
const stream = options.adapter.roomStream;
|
|
2559
|
+
if (stream === undefined || withdrawStream !== undefined)
|
|
2560
|
+
return;
|
|
2561
|
+
withdrawStream = stream.observe((event) => {
|
|
2562
|
+
if (closed || stopping)
|
|
2563
|
+
return;
|
|
2564
|
+
if (event.kind === 'opened') {
|
|
2565
|
+
socketOpen = true;
|
|
2566
|
+
generation += 1;
|
|
2567
|
+
conversations.reopened();
|
|
2568
|
+
recovery.resume();
|
|
2569
|
+
void sync('opened');
|
|
2570
|
+
}
|
|
2571
|
+
else if (event.kind === 'closed')
|
|
2572
|
+
closedSocket(event.revoked);
|
|
2573
|
+
else if (event.kind === 'opening')
|
|
2574
|
+
void applyOpening(event.expired);
|
|
2575
|
+
else
|
|
2576
|
+
void applyRoom(event.locator, event.roomVersion);
|
|
2577
|
+
}, receipts);
|
|
2578
|
+
}
|
|
1791
2579
|
function writesHeld() {
|
|
1792
2580
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
1793
2581
|
}
|
|
@@ -1806,14 +2594,17 @@ export function composeRuntime(options) {
|
|
|
1806
2594
|
listener(mode);
|
|
1807
2595
|
}
|
|
1808
2596
|
if (releasing)
|
|
1809
|
-
|
|
2597
|
+
submitAll();
|
|
1810
2598
|
}
|
|
1811
2599
|
function observeConversations() {
|
|
1812
2600
|
const stream = options.adapter.conversations;
|
|
1813
2601
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
1814
2602
|
return;
|
|
1815
2603
|
withdrawConversations = stream.observe((event) => {
|
|
1816
|
-
conversations.receive(event)
|
|
2604
|
+
if (conversations.receive(event)) {
|
|
2605
|
+
void ready.then(scheduleCurrency);
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
1817
2608
|
if (event.kind !== 'gap' || conversations.current())
|
|
1818
2609
|
return;
|
|
1819
2610
|
show();
|
|
@@ -1821,129 +2612,42 @@ export function composeRuntime(options) {
|
|
|
1821
2612
|
});
|
|
1822
2613
|
}
|
|
1823
2614
|
function scheduleCurrency() {
|
|
1824
|
-
if (currencyQueued || closed || stopping)
|
|
2615
|
+
if (currencyQueued === generation || closed || stopping)
|
|
1825
2616
|
return;
|
|
1826
|
-
currencyQueued =
|
|
1827
|
-
|
|
2617
|
+
currencyQueued = generation;
|
|
2618
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
1828
2619
|
}
|
|
1829
|
-
async function runCurrency() {
|
|
1830
|
-
currencyQueued
|
|
1831
|
-
|
|
2620
|
+
async function runCurrency(live) {
|
|
2621
|
+
if (currencyQueued === generation)
|
|
2622
|
+
currencyQueued = null;
|
|
2623
|
+
if (!live())
|
|
1832
2624
|
return;
|
|
1833
2625
|
await conversations.reconcile();
|
|
1834
2626
|
if (conversations.current()) {
|
|
1835
2627
|
show();
|
|
1836
2628
|
return;
|
|
1837
2629
|
}
|
|
1838
|
-
const
|
|
1839
|
-
|
|
1840
|
-
moveTo(transferring(resting));
|
|
2630
|
+
const progress = { complete: false, reachable: !status.providerUnreachable };
|
|
2631
|
+
moveTo(transferring(status));
|
|
1841
2632
|
try {
|
|
1842
2633
|
await conversations.catchUp(progress);
|
|
1843
2634
|
}
|
|
1844
2635
|
catch {
|
|
1845
2636
|
progress.reachable = false;
|
|
1846
2637
|
}
|
|
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;
|
|
2638
|
+
await rest(progress, live);
|
|
1935
2639
|
}
|
|
1936
2640
|
function close() {
|
|
1937
2641
|
if (closed)
|
|
1938
2642
|
return;
|
|
1939
2643
|
closed = true;
|
|
1940
|
-
|
|
1941
|
-
|
|
2644
|
+
withdrawStream?.();
|
|
2645
|
+
withdrawStream = undefined;
|
|
1942
2646
|
withdrawConversations?.();
|
|
1943
2647
|
withdrawConversations = undefined;
|
|
1944
2648
|
withdrawServiceMode?.();
|
|
1945
2649
|
conversations.close();
|
|
1946
|
-
|
|
2650
|
+
void recovery.retire();
|
|
1947
2651
|
store?.close();
|
|
1948
2652
|
void opening?.then((opened) => {
|
|
1949
2653
|
opened.close();
|
|
@@ -1953,6 +2657,7 @@ export function composeRuntime(options) {
|
|
|
1953
2657
|
storageLossListeners.clear();
|
|
1954
2658
|
statusListeners.clear();
|
|
1955
2659
|
serviceModeListeners.clear();
|
|
2660
|
+
memberListeners.clear();
|
|
1956
2661
|
readSide.close();
|
|
1957
2662
|
queueSubscriptions.clear();
|
|
1958
2663
|
roomDraftListeners.clear();
|
|
@@ -1960,8 +2665,8 @@ export function composeRuntime(options) {
|
|
|
1960
2665
|
}
|
|
1961
2666
|
async function stop() {
|
|
1962
2667
|
stopping = true;
|
|
1963
|
-
|
|
1964
|
-
await
|
|
2668
|
+
await recovery.retire();
|
|
2669
|
+
await Promise.all(working);
|
|
1965
2670
|
await mutations;
|
|
1966
2671
|
close();
|
|
1967
2672
|
}
|
|
@@ -1970,11 +2675,69 @@ export function composeRuntime(options) {
|
|
|
1970
2675
|
options.signal?.addEventListener('abort', close, { once: true });
|
|
1971
2676
|
if (options.signal?.aborted)
|
|
1972
2677
|
close();
|
|
1973
|
-
|
|
2678
|
+
observeStream();
|
|
2679
|
+
void openLocal().then(() => {
|
|
2680
|
+
if (capabilities.length > 0)
|
|
2681
|
+
notifyReady();
|
|
2682
|
+
}, () => {
|
|
1974
2683
|
notifyReady();
|
|
1975
2684
|
});
|
|
1976
|
-
|
|
1977
|
-
|
|
2685
|
+
if (options.adapter.roomStream === undefined)
|
|
2686
|
+
void sync('unstreamed');
|
|
2687
|
+
function planned(input, reach) {
|
|
2688
|
+
return planOf(input, roomFacts(reach), planReach(reach));
|
|
2689
|
+
}
|
|
2690
|
+
function watchPlan(plan, deliver) {
|
|
2691
|
+
return readSide.watch(plan, identityOf(plan), () => {
|
|
2692
|
+
const cover = covered(plan);
|
|
2693
|
+
awaitPreparation(cover);
|
|
2694
|
+
return cover;
|
|
2695
|
+
}, deliver);
|
|
2696
|
+
}
|
|
2697
|
+
async function searched(input, reach) {
|
|
2698
|
+
const plan = searchPlanOf(input);
|
|
2699
|
+
const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
|
|
2700
|
+
for (;;) {
|
|
2701
|
+
const cover = covered(filter);
|
|
2702
|
+
awaitPreparation(cover);
|
|
2703
|
+
const pending = 'pending' in cover ? [...cover.pending] : [];
|
|
2704
|
+
const covering = 'pending' in cover ? [] : cover.coverage.rooms;
|
|
2705
|
+
const unindexed = covering
|
|
2706
|
+
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
2707
|
+
.map(({ room }) => room);
|
|
2708
|
+
for (const room of unindexed)
|
|
2709
|
+
pending.push({ room, awaits: 'index' });
|
|
2710
|
+
const held = conversations.searchable();
|
|
2711
|
+
const named = reach !== null && !reach.conversations
|
|
2712
|
+
? []
|
|
2713
|
+
: (plan.conversations ?? held).filter((one) => held.includes(one) && conversations.acquired(one));
|
|
2714
|
+
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
2715
|
+
for (const conversation of waiting)
|
|
2716
|
+
pending.push({ conversation, awaits: 'index' });
|
|
2717
|
+
if (unindexed.length > 0 || waiting.length > 0)
|
|
2718
|
+
index(unindexed, waiting);
|
|
2719
|
+
if (pending.length > 0 || 'pending' in cover)
|
|
2720
|
+
return { pending };
|
|
2721
|
+
const seen = searchIndex.changes();
|
|
2722
|
+
await searchIndex.caughtUp();
|
|
2723
|
+
const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(filter);
|
|
2724
|
+
if (selected !== null && 'pending' in selected)
|
|
2725
|
+
return selected;
|
|
2726
|
+
if (searchIndex.changes() !== seen ||
|
|
2727
|
+
JSON.stringify(covered(filter)) !== JSON.stringify(cover))
|
|
2728
|
+
continue;
|
|
2729
|
+
return {
|
|
2730
|
+
...searchIndex.search(plan, selected === null ? null : selected.documents, named),
|
|
2731
|
+
coverage: {
|
|
2732
|
+
rooms: covering,
|
|
2733
|
+
conversations: named,
|
|
2734
|
+
index: searchIndex.generation(),
|
|
2735
|
+
overlay: cover.coverage.overlay,
|
|
2736
|
+
},
|
|
2737
|
+
};
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
const watch = (topic, input, deliver) => {
|
|
1978
2741
|
if (topic === 'room-drafts') {
|
|
1979
2742
|
const listener = { notify: deliver, ready: false };
|
|
1980
2743
|
roomDraftListeners.add(listener);
|
|
@@ -1995,6 +2758,10 @@ export function composeRuntime(options) {
|
|
|
1995
2758
|
if (topic === 'conversation') {
|
|
1996
2759
|
return conversations.watch(input[0], input[1], deliver);
|
|
1997
2760
|
}
|
|
2761
|
+
if (topic === 'conversations')
|
|
2762
|
+
return conversations.watchList(deliver);
|
|
2763
|
+
if (topic === 'agents')
|
|
2764
|
+
return conversations.watchAgents(deliver);
|
|
1998
2765
|
if (topic === 'presentation') {
|
|
1999
2766
|
const listener = {
|
|
2000
2767
|
app: input[0],
|
|
@@ -2049,6 +2816,13 @@ export function composeRuntime(options) {
|
|
|
2049
2816
|
serviceModeListeners.delete(deliver);
|
|
2050
2817
|
};
|
|
2051
2818
|
}
|
|
2819
|
+
if (topic === 'member') {
|
|
2820
|
+
memberListeners.add(deliver);
|
|
2821
|
+
deliver(member);
|
|
2822
|
+
return () => {
|
|
2823
|
+
memberListeners.delete(deliver);
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2052
2826
|
if (topic === 'queue') {
|
|
2053
2827
|
const queue = { locator: input[0], notify: deliver };
|
|
2054
2828
|
queueSubscriptions.add(queue);
|
|
@@ -2057,9 +2831,8 @@ export function composeRuntime(options) {
|
|
|
2057
2831
|
queueSubscriptions.delete(queue);
|
|
2058
2832
|
};
|
|
2059
2833
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
}
|
|
2834
|
+
return watchPlan(planned(input[0], null), deliver);
|
|
2835
|
+
};
|
|
2063
2836
|
const md = {
|
|
2064
2837
|
loaded() {
|
|
2065
2838
|
return openLocal().then(() => undefined);
|
|
@@ -2067,18 +2840,8 @@ export function composeRuntime(options) {
|
|
|
2067
2840
|
rooms() {
|
|
2068
2841
|
return listed();
|
|
2069
2842
|
},
|
|
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
|
-
},
|
|
2843
|
+
query: ((input) => rowsQuery(planned(input, null))),
|
|
2844
|
+
search: (input) => searched(input, null),
|
|
2082
2845
|
types(locator) {
|
|
2083
2846
|
return {
|
|
2084
2847
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -2088,12 +2851,6 @@ export function composeRuntime(options) {
|
|
|
2088
2851
|
: modelTypes(modelOf(locator, 'typed-selection')),
|
|
2089
2852
|
};
|
|
2090
2853
|
},
|
|
2091
|
-
syncStatus() {
|
|
2092
|
-
return shown;
|
|
2093
|
-
},
|
|
2094
|
-
serviceMode() {
|
|
2095
|
-
return serviceMode;
|
|
2096
|
-
},
|
|
2097
2854
|
roomDraft: {
|
|
2098
2855
|
list() {
|
|
2099
2856
|
return exclusive(async () => {
|
|
@@ -2112,20 +2869,20 @@ export function composeRuntime(options) {
|
|
|
2112
2869
|
return exclusive(async () => {
|
|
2113
2870
|
const parts = readParts(input);
|
|
2114
2871
|
if (path === '' || normalize(path) !== path)
|
|
2115
|
-
throw
|
|
2872
|
+
throw runtimeError('room-draft-malformed');
|
|
2116
2873
|
const local = await openLocal();
|
|
2117
2874
|
const current = roomDrafts.get(room)?.get(path);
|
|
2118
2875
|
if ((current?.revision ?? null) !== revision) {
|
|
2119
|
-
return
|
|
2120
|
-
outcome: 'stale',
|
|
2121
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2122
|
-
};
|
|
2876
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2123
2877
|
}
|
|
2878
|
+
const file = parts.file === undefined
|
|
2879
|
+
? undefined
|
|
2880
|
+
: await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
|
|
2124
2881
|
const record = {
|
|
2125
2882
|
locator: room,
|
|
2126
2883
|
path,
|
|
2127
2884
|
revision: roomDraftRevision(),
|
|
2128
|
-
parts: held(current?.parts, parts),
|
|
2885
|
+
parts: held(current?.parts, parts, file),
|
|
2129
2886
|
returned: current?.returned ?? null,
|
|
2130
2887
|
};
|
|
2131
2888
|
await local.keepRoomDraft(record);
|
|
@@ -2139,10 +2896,7 @@ export function composeRuntime(options) {
|
|
|
2139
2896
|
const local = await openLocal();
|
|
2140
2897
|
const current = roomDrafts.get(room)?.get(path);
|
|
2141
2898
|
if (current === undefined || current.revision !== revision) {
|
|
2142
|
-
return
|
|
2143
|
-
outcome: 'stale',
|
|
2144
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2145
|
-
};
|
|
2899
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2146
2900
|
}
|
|
2147
2901
|
await local.discardRoomDraft(room, path);
|
|
2148
2902
|
forgetRoomDraft(room, path);
|
|
@@ -2174,12 +2928,13 @@ export function composeRuntime(options) {
|
|
|
2174
2928
|
save(room, named, writtenBy) {
|
|
2175
2929
|
return exclusive(() => saveRoomDrafts(room, named, writtenBy));
|
|
2176
2930
|
},
|
|
2177
|
-
outcome(identifier) {
|
|
2178
|
-
return outcomes.get(identifier) ?? null;
|
|
2179
|
-
},
|
|
2180
2931
|
history(locator, path, query) {
|
|
2181
2932
|
return readHistory(locator, path, query);
|
|
2182
2933
|
},
|
|
2934
|
+
binaryFile: {
|
|
2935
|
+
read: readBinaryPath,
|
|
2936
|
+
readReference: readBinaryReference,
|
|
2937
|
+
},
|
|
2183
2938
|
call(name, input) {
|
|
2184
2939
|
return invokeAction(name, input);
|
|
2185
2940
|
},
|
|
@@ -2191,25 +2946,34 @@ export function composeRuntime(options) {
|
|
|
2191
2946
|
log() {
|
|
2192
2947
|
return journal.export();
|
|
2193
2948
|
},
|
|
2194
|
-
storageLoss() {
|
|
2195
|
-
return storageLoss;
|
|
2196
|
-
},
|
|
2197
2949
|
persistence() {
|
|
2198
2950
|
return options.shell.persistentStorage;
|
|
2199
2951
|
},
|
|
2200
|
-
observe: (
|
|
2201
|
-
deliver(structuredClone(message));
|
|
2202
|
-
}), topic === 'query')),
|
|
2952
|
+
observe: observing(watch),
|
|
2203
2953
|
async reauthorize() {
|
|
2204
2954
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2205
2955
|
return;
|
|
2206
2956
|
journal.record('connection reauthorize');
|
|
2207
|
-
await
|
|
2957
|
+
await Promise.all([...lanes.values()]
|
|
2958
|
+
.filter((held) => held.generation === generation)
|
|
2959
|
+
.map((held) => held.chain));
|
|
2208
2960
|
await mutations;
|
|
2209
2961
|
authorization = undefined;
|
|
2210
2962
|
discoveryPaused = false;
|
|
2211
|
-
|
|
2963
|
+
if (options.adapter.roomStream === undefined)
|
|
2964
|
+
await sync('unstreamed');
|
|
2965
|
+
else
|
|
2966
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2212
2967
|
},
|
|
2213
2968
|
};
|
|
2214
|
-
|
|
2969
|
+
const reachable = {
|
|
2970
|
+
md,
|
|
2971
|
+
watch,
|
|
2972
|
+
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
2973
|
+
plan: planned,
|
|
2974
|
+
query: (plan) => rowsQuery(plan),
|
|
2975
|
+
watchPlan,
|
|
2976
|
+
search: searched,
|
|
2977
|
+
};
|
|
2978
|
+
return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2215
2979
|
}
|