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