@oar-sh/oar 0.9.8 → 0.9.10
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/CHANGELOG.md +478 -0
- package/README.md +129 -39
- package/bin/bat/oar.bat +46 -46
- package/bin/oar.js +814 -228
- package/package.json +5 -6
- package/server/README.md +306 -4
- package/server/claude-worker/claude-remote-relay-tool.mjs +10 -0
- package/server/claude-worker/claude-session-process.mjs +35 -1
- package/server/claude-worker/claude-turn-publisher.mjs +3 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +31 -4
- package/server/claude-worker/subagent-timeline-recorder.mjs +572 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +3 -1
- package/server/cursor-worker/cursor-sdk-adapter.mjs +56 -2
- package/server/db-schema.mjs +44 -0
- package/server/migrations/0008-message-pins.mjs +85 -0
- package/server/migrations/0009-subagent-timeline.mjs +135 -0
- package/server/migrations/0010-subagent-timeline-pruning.mjs +89 -0
- package/server/migrations/0011-relay-event-subscriptions.mjs +125 -0
- package/server/migrations/0012-cross-relay-event-subscriptions.mjs +116 -0
- package/server/public/app/agent-exchange-fold-view.mjs +248 -0
- package/server/public/app/api-client.js +91 -0
- package/server/public/app/archive-confirm.mjs +28 -0
- package/server/public/app/ask-user-view.js +3 -0
- package/server/public/app/attachments-view.js +1024 -131
- package/server/public/app/back-guard.mjs +58 -0
- package/server/public/app/background-tasks-view.mjs +18 -26
- package/server/public/app/bootstrap.js +230 -35
- package/server/public/app/chat-actions-menu-state.mjs +116 -0
- package/server/public/app/claude-cloud-conversation-ui.js +10 -22
- package/server/public/app/composer-control-state.mjs +16 -0
- package/server/public/app/composer-keyboard.mjs +54 -0
- package/server/public/app/conversation-view.js +471 -110
- package/server/public/app/explorer-edit-state.mjs +68 -0
- package/server/public/app/external-link-policy.mjs +4 -1
- package/server/public/app/file-version.mjs +58 -0
- package/server/public/app/host-suspend-ui.js +6 -3
- package/server/public/app/journal-view.js +161 -27
- package/server/public/app/no-conversation-state.mjs +58 -0
- package/server/public/app/pinned-messages-view.mjs +253 -0
- package/server/public/app/provider-readiness.mjs +120 -0
- package/server/public/app/pwa-install.js +1 -1
- package/server/public/app/relay-board-view.js +3 -0
- package/server/public/app/relay-dot-state.mjs +8 -1
- package/server/public/app/relay-event-subscriptions-ui.js +184 -0
- package/server/public/app/relay-event-view.mjs +234 -0
- package/server/public/app/router.js +8 -2
- package/server/public/app/settings-modal.js +53 -1
- package/server/public/app/socket-handlers.js +65 -28
- package/server/public/app/store.js +25 -2
- package/server/public/app/subagent-row.mjs +138 -0
- package/server/public/app/subagent-timeline-view.mjs +186 -0
- package/server/public/app/subagent-viewer-watch.mjs +50 -0
- package/server/public/app/subagent-viewer.mjs +534 -0
- package/server/public/app/task-row-format.mjs +29 -0
- package/server/public/app/thinking-follow.js +4 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +290 -0
- package/server/public/app/transcript-jump-bottom.mjs +99 -0
- package/server/public/app/transcript-reload-freshness.mjs +25 -0
- package/server/public/app/update-ui.js +1 -1
- package/server/public/app/viewer-chat-gallery.mjs +59 -0
- package/server/public/app/viewer-controls-state.mjs +59 -0
- package/server/public/app/viewer-gallery.mjs +46 -0
- package/server/public/app/viewer-gestures.mjs +105 -0
- package/server/public/app/viewer-history.mjs +45 -0
- package/server/public/app/viewer-tap.mjs +112 -0
- package/server/public/index.html +700 -59
- package/server/repositories/message-pin-statements.mjs +39 -0
- package/server/repositories/message-repository.mjs +34 -3
- package/server/repositories/question-repository.mjs +9 -3
- package/server/repositories/remote-relay-repository.mjs +6 -2
- package/server/repositories/subagent-timeline-statements.mjs +170 -0
- package/server/routes/ask-user-routes.mjs +3 -0
- package/server/routes/messages-routes.mjs +548 -111
- package/server/routes/relay-event-routes.mjs +157 -0
- package/server/routes/remote-relay-routes.mjs +15 -0
- package/server/routes/sessions-routes.mjs +164 -24
- package/server/server-runtime.mjs +274 -62
- package/server/services/cloudflared-tunnel-service.mjs +139 -41
- package/server/services/explorer-entry-name.mjs +26 -0
- package/server/services/explorer-mutations.mjs +122 -0
- package/server/services/file-serving.mjs +155 -0
- package/server/services/message-pin-list.mjs +52 -0
- package/server/services/oar-cli-helpers.mjs +139 -4
- package/server/services/oar-cli-menu.mjs +240 -0
- package/server/services/oar-cli-wsl.mjs +129 -0
- package/server/services/preview-proxy-server.mjs +41 -23
- package/server/services/push-dispatch-service.mjs +18 -0
- package/server/services/relay-event-link-service.mjs +289 -0
- package/server/services/relay-event-service.mjs +1935 -0
- package/server/services/relay-shutdown-gate.mjs +31 -0
- package/server/services/remote-relay-caller-context.mjs +52 -7
- package/server/services/remote-relay-dispatcher.mjs +494 -2
- package/server/services/remote-relay-registry-service.mjs +11 -1
- package/server/services/safe-served-content.mjs +61 -11
- package/server/services/session-activity-tracker.mjs +487 -0
- package/server/services/session-history-refresh-service.mjs +31 -0
- package/server/services/stale-attempt-log-limiter.mjs +96 -0
- package/server/services/stale-recovery-clock-guard.mjs +57 -0
- package/server/services/stale-subagent-run-sweep.mjs +109 -0
- package/server/services/subagent-record-retention.mjs +164 -0
- package/server/services/subagent-timeline-service.mjs +328 -0
- package/server/services/subagent-viewer-rooms.mjs +119 -0
- package/server/services/upgrade-socket-guard.mjs +16 -0
- package/shared/relay-events.mjs +371 -0
- package/shared/remote-relay-contract.mjs +157 -5
- package/shared/subagent-record-retention.mjs +31 -0
- package/shared/subagent-timeline.mjs +79 -0
|
@@ -324,6 +324,22 @@ export function createPushDispatchService({
|
|
|
324
324
|
}
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
+
// A turn that answers relay events (its message is the relay's event row,
|
|
328
|
+
// kind 'relay-event') pushes no "reply ready" or "failed" either: the
|
|
329
|
+
// watcher answers events many times an hour and pushes by itself when one
|
|
330
|
+
// matters. Question pushes are unchanged.
|
|
331
|
+
let messageKindStatement = null;
|
|
332
|
+
function turnAnswersRelayEvents(sourceMessageId) {
|
|
333
|
+
const id = String(sourceMessageId || '').trim();
|
|
334
|
+
if (!id) return false;
|
|
335
|
+
try {
|
|
336
|
+
messageKindStatement ??= db.prepare(`SELECT kind FROM messages WHERE id = ?`);
|
|
337
|
+
return String(messageKindStatement.get(id)?.kind || '').trim() === 'relay-event';
|
|
338
|
+
} catch {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
327
343
|
async function sendToSubscription(row, event) {
|
|
328
344
|
const preferences = normalizePushPreferences(parseJsonObject(row.preferences_json));
|
|
329
345
|
const payload = renderPushNotification(event, preferences);
|
|
@@ -440,6 +456,7 @@ export function createPushDispatchService({
|
|
|
440
456
|
// the turn.
|
|
441
457
|
function notifyTurnComplete({ conversationId, messageId, sourceMessageId, text } = {}) {
|
|
442
458
|
if (turnStartedByRemoteAgent(sourceMessageId)) return Promise.resolve({ outcome: 'remote-agent' });
|
|
459
|
+
if (turnAnswersRelayEvents(sourceMessageId)) return Promise.resolve({ outcome: 'relay-event' });
|
|
443
460
|
return dispatch({
|
|
444
461
|
type: 'turnComplete',
|
|
445
462
|
conversationId: conversationId || null,
|
|
@@ -450,6 +467,7 @@ export function createPushDispatchService({
|
|
|
450
467
|
|
|
451
468
|
function notifyTurnFailed({ conversationId, messageId, sourceMessageId, text } = {}) {
|
|
452
469
|
if (turnStartedByRemoteAgent(sourceMessageId)) return Promise.resolve({ outcome: 'remote-agent' });
|
|
470
|
+
if (turnAnswersRelayEvents(sourceMessageId)) return Promise.resolve({ outcome: 'relay-event' });
|
|
453
471
|
return dispatch({
|
|
454
472
|
type: 'turnFailed',
|
|
455
473
|
conversationId: conversationId || null,
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The watching side of relay event subscriptions on paired relays: while
|
|
5
|
+
* this relay holds subscriptions on a paired relay (or an end it has not
|
|
6
|
+
* delivered there yet), it fetches that relay's events every few seconds
|
|
7
|
+
* over the pairing and hands them to the event service, which delivers them
|
|
8
|
+
* like the events of this relay's own sessions. Only this relay calls, so a
|
|
9
|
+
* pairing in one direction is enough.
|
|
10
|
+
*
|
|
11
|
+
* A fetch (POST /api/relay-events/remote/pull) acknowledges what the last
|
|
12
|
+
* one stored, carries the ends and watcher names the other relay must
|
|
13
|
+
* learn, and comes back with new events and the subscriptions' state; the
|
|
14
|
+
* event service writes all of it and the cursor in one transaction, so
|
|
15
|
+
* nothing is lost or delivered twice when either relay restarts.
|
|
16
|
+
*
|
|
17
|
+
* A link with no answer for remoteGapMs (failing fetches, or this relay
|
|
18
|
+
* down) is lost from its last answer on. The first fetch after that, every
|
|
19
|
+
* page of it, is held and delivered as one replayed row per watcher with
|
|
20
|
+
* link_lost and link_restored around it. A shorter outage only delays.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { REMOTE_RELAY_ERROR_CODES } from '../../shared/remote-relay-contract.mjs';
|
|
24
|
+
import { RELAY_EVENT_LIMITS } from '../../shared/relay-events.mjs';
|
|
25
|
+
|
|
26
|
+
export const RELAY_EVENT_REMOTE_SUBSCRIBE_PATH = '/api/relay-events/remote/subscribe';
|
|
27
|
+
export const RELAY_EVENT_REMOTE_PULL_PATH = '/api/relay-events/remote/pull';
|
|
28
|
+
|
|
29
|
+
const LOG_PREFIX = '[relay-events]';
|
|
30
|
+
// Pages of one fetch before the next tick goes on (remotePullBatch each).
|
|
31
|
+
const MAX_PAGES_PER_FETCH = 50;
|
|
32
|
+
|
|
33
|
+
function toText(value) {
|
|
34
|
+
return String(value ?? '').trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A paired relay that predates cross-relay subscriptions answers their
|
|
39
|
+
* routes with Express's HTML 404: no JSON, no code. A 404 of these routes
|
|
40
|
+
* always carries one.
|
|
41
|
+
*/
|
|
42
|
+
export function isMissingRelayEventRoute(error) {
|
|
43
|
+
return toText(error?.code) === REMOTE_RELAY_ERROR_CODES.notFound && !toText(error?.remoteBody?.code);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function relayWithoutCrossRelayEventsText(name) {
|
|
47
|
+
return `Relay "${toText(name) || 'the paired relay'}" runs an OAR version without event subscriptions across relays; it needs an update before its sessions can be watched from here.`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function describeFailure(relay, error) {
|
|
51
|
+
if (isMissingRelayEventRoute(error)) return relayWithoutCrossRelayEventsText(relay?.name);
|
|
52
|
+
return toText(error?.message || error).slice(0, 300) || 'no answer';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isPullAnswer(response) {
|
|
56
|
+
return !!response && typeof response === 'object' && Array.isArray(response.events) && Array.isArray(response.subscriptions);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createRelayEventLinkService({
|
|
60
|
+
relayEvents,
|
|
61
|
+
// `get(id)`: the registry entry (address and token) of a paired relay.
|
|
62
|
+
registry,
|
|
63
|
+
// `request(relay, method, path, { body, timeoutMs })`: remote-relay-client.mjs.
|
|
64
|
+
client,
|
|
65
|
+
now = () => Date.now(),
|
|
66
|
+
setTimer = (fn, ms) => {
|
|
67
|
+
const timer = setTimeout(fn, ms);
|
|
68
|
+
timer.unref?.();
|
|
69
|
+
return timer;
|
|
70
|
+
},
|
|
71
|
+
clearTimer = (timer) => clearTimeout(timer),
|
|
72
|
+
logger = console,
|
|
73
|
+
pollMs = RELAY_EVENT_LIMITS.remotePollMs,
|
|
74
|
+
failingPollMs = RELAY_EVENT_LIMITS.remotePollFailingMs,
|
|
75
|
+
gapMs = RELAY_EVENT_LIMITS.remoteGapMs,
|
|
76
|
+
timeoutMs = 15_000,
|
|
77
|
+
} = {}) {
|
|
78
|
+
if (!relayEvents) throw new Error('relayEvents is required');
|
|
79
|
+
// paired relay id → { timer, running, again, replay }
|
|
80
|
+
const links = new Map();
|
|
81
|
+
let started = false;
|
|
82
|
+
|
|
83
|
+
function warn(line) {
|
|
84
|
+
try { logger?.warn?.(`${LOG_PREFIX} ${line}`); } catch {}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function linkState(relayId) {
|
|
88
|
+
if (!links.has(relayId)) links.set(relayId, { timer: null, running: null, again: false, replay: null, replayApplied: false });
|
|
89
|
+
return links.get(relayId);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function forget(relayId) {
|
|
93
|
+
const link = links.get(relayId);
|
|
94
|
+
if (link?.timer) clearTimer(link.timer);
|
|
95
|
+
links.delete(relayId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function schedule(relayId, delayMs) {
|
|
99
|
+
if (!started) return;
|
|
100
|
+
const link = linkState(relayId);
|
|
101
|
+
if (link.timer) clearTimer(link.timer);
|
|
102
|
+
link.timer = setTimer(() => {
|
|
103
|
+
link.timer = null;
|
|
104
|
+
void fetchNow(relayId);
|
|
105
|
+
}, Math.max(0, delayMs));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function wanted(relayId) {
|
|
109
|
+
return relayEvents.remoteRelaysToPoll().includes(relayId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function failingForLong(relayId) {
|
|
113
|
+
const first = Date.parse(String(relayEvents.getRemoteLink(relayId).first_fail_at || ''));
|
|
114
|
+
return Number.isFinite(first) && now() - first >= gapMs;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Whether the link has been without an answer long enough to replay: since when. */
|
|
118
|
+
function gapSince(relayId) {
|
|
119
|
+
const health = relayEvents.getRemoteLink(relayId);
|
|
120
|
+
if (health.lost_at) return health.lost_at;
|
|
121
|
+
const lastOk = Date.parse(String(health.last_ok_at || ''));
|
|
122
|
+
return Number.isFinite(lastOk) && now() - lastOk >= gapMs ? health.last_ok_at : null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function startReplay(link, relayId, since) {
|
|
126
|
+
const perWatcher = new Map();
|
|
127
|
+
for (const [watcher, subscriptionId] of relayEvents.remoteWatchers(relayId)) {
|
|
128
|
+
perWatcher.set(watcher, { subscriptionId, kept: 0, dropped: 0, ended: [] });
|
|
129
|
+
}
|
|
130
|
+
link.replay = { since, perWatcher };
|
|
131
|
+
link.replayApplied = false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function addToReplay(link, relayId, applied) {
|
|
135
|
+
for (const [watcher, info] of applied) {
|
|
136
|
+
let entry = link.replay.perWatcher.get(watcher);
|
|
137
|
+
if (!entry) {
|
|
138
|
+
const subscriptionId = relayEvents.remoteWatchers(relayId).get(watcher);
|
|
139
|
+
if (!subscriptionId) continue;
|
|
140
|
+
entry = { subscriptionId, kept: 0, dropped: 0, ended: [] };
|
|
141
|
+
link.replay.perWatcher.set(watcher, entry);
|
|
142
|
+
}
|
|
143
|
+
entry.kept += Number(info.kept) || 0;
|
|
144
|
+
entry.dropped += Number(info.dropped) || 0;
|
|
145
|
+
entry.ended.push(...(info.ended || []));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function fetchPages(relayId, link) {
|
|
150
|
+
const relay = registry?.get?.(relayId) || null;
|
|
151
|
+
if (!relay) {
|
|
152
|
+
// The pairing went without the hook (or before this boot).
|
|
153
|
+
relayEvents.noteRelayRemoved(relayId);
|
|
154
|
+
return { ok: false, removed: true, error: 'The pairing with that relay was removed.' };
|
|
155
|
+
}
|
|
156
|
+
for (let page = 0; page < MAX_PAGES_PER_FETCH; page += 1) {
|
|
157
|
+
const body = relayEvents.buildRemotePull(relayId);
|
|
158
|
+
if (!body.subscriptions.length && !body.ended.length && !link.replay) return { ok: true, idle: true };
|
|
159
|
+
if (!link.replay) {
|
|
160
|
+
const since = gapSince(relayId);
|
|
161
|
+
if (since) startReplay(link, relayId, since);
|
|
162
|
+
}
|
|
163
|
+
let response;
|
|
164
|
+
try {
|
|
165
|
+
response = await client.request(relay, 'POST', RELAY_EVENT_REMOTE_PULL_PATH, { body, timeoutMs });
|
|
166
|
+
if (!isPullAnswer(response)) throw new Error(`Relay "${toText(relay.name)}" gave an unexpected answer to the event fetch`);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
const text = describeFailure(relay, error);
|
|
169
|
+
relayEvents.noteRemoteLinkFailure(relayId, text);
|
|
170
|
+
// Nothing of a replay was written yet: the next fetch starts it again.
|
|
171
|
+
if (link.replay && !link.replayApplied) link.replay = null;
|
|
172
|
+
return { ok: false, error: text, unsupported: isMissingRelayEventRoute(error) };
|
|
173
|
+
}
|
|
174
|
+
const applied = relayEvents.applyRemotePull(relayId, { relayName: relay.name, response, replay: !!link.replay });
|
|
175
|
+
if (link.replay) {
|
|
176
|
+
link.replayApplied = true;
|
|
177
|
+
addToReplay(link, relayId, applied);
|
|
178
|
+
}
|
|
179
|
+
if (response.more !== true) break;
|
|
180
|
+
}
|
|
181
|
+
if (link.replay) {
|
|
182
|
+
const { since, perWatcher } = link.replay;
|
|
183
|
+
link.replay = null;
|
|
184
|
+
link.replayApplied = false;
|
|
185
|
+
relayEvents.noteRemoteLinkRestored(relayId, { relayName: relay.name, since, perWatcher });
|
|
186
|
+
}
|
|
187
|
+
return { ok: true };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Fetches from a paired relay now (a fetch already running is followed by
|
|
192
|
+
* one more). Resolves `{ ok, error?, idle?, unsupported? }`; never throws.
|
|
193
|
+
*/
|
|
194
|
+
function fetchNow(relayId) {
|
|
195
|
+
const id = toText(relayId);
|
|
196
|
+
const link = linkState(id);
|
|
197
|
+
if (link.timer) {
|
|
198
|
+
clearTimer(link.timer);
|
|
199
|
+
link.timer = null;
|
|
200
|
+
}
|
|
201
|
+
if (link.running) {
|
|
202
|
+
link.again = true;
|
|
203
|
+
return link.running;
|
|
204
|
+
}
|
|
205
|
+
link.running = (async () => {
|
|
206
|
+
let result;
|
|
207
|
+
try {
|
|
208
|
+
result = await fetchPages(id, link);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
warn(`fetch from paired relay ${id.slice(0, 12)} failed: ${toText(error?.message || error).slice(0, 200)}`);
|
|
211
|
+
result = { ok: false, error: toText(error?.message || error) };
|
|
212
|
+
} finally {
|
|
213
|
+
link.running = null;
|
|
214
|
+
}
|
|
215
|
+
if (result.removed) {
|
|
216
|
+
forget(id);
|
|
217
|
+
} else if (link.again) {
|
|
218
|
+
link.again = false;
|
|
219
|
+
schedule(id, 0);
|
|
220
|
+
} else if (wanted(id)) {
|
|
221
|
+
schedule(id, result.ok ? pollMs : (failingForLong(id) ? failingPollMs : pollMs));
|
|
222
|
+
} else if (!link.replay) {
|
|
223
|
+
forget(id);
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
})();
|
|
227
|
+
return link.running;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** A fetch, waited for at most `waitMs` (list_subscriptions wants fresh state). */
|
|
231
|
+
async function refresh(relayId, { waitMs = 5_000 } = {}) {
|
|
232
|
+
let timer = null;
|
|
233
|
+
const timeout = new Promise((resolve) => {
|
|
234
|
+
timer = setTimer(() => resolve({ ok: false, error: 'timeout' }), waitMs);
|
|
235
|
+
});
|
|
236
|
+
try {
|
|
237
|
+
return await Promise.race([fetchNow(relayId), timeout]);
|
|
238
|
+
} finally {
|
|
239
|
+
clearTimer(timer);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The pairing with a relay is being removed (`entry` still has its address
|
|
245
|
+
* and token): one last fetch tells it every subscription ended, then the
|
|
246
|
+
* mirrors end here. A relay that does not answer ends them itself after
|
|
247
|
+
* remoteKeepDays without a fetch.
|
|
248
|
+
*/
|
|
249
|
+
async function noteRelayRemoved(entry) {
|
|
250
|
+
const relayId = toText(entry?.id);
|
|
251
|
+
if (!relayId) return;
|
|
252
|
+
forget(relayId);
|
|
253
|
+
const rows = relayEvents.mirrorsOf(relayId);
|
|
254
|
+
if (rows.length && entry?.url) {
|
|
255
|
+
const body = {
|
|
256
|
+
after: Number(relayEvents.getRemoteLink(relayId).cursor) || 0,
|
|
257
|
+
subscriptions: [],
|
|
258
|
+
ended: rows.map((row) => row.remote_subscription_id),
|
|
259
|
+
};
|
|
260
|
+
try {
|
|
261
|
+
await client.request(entry, 'POST', RELAY_EVENT_REMOTE_PULL_PATH, { body, timeoutMs: Math.min(timeoutMs, 10_000) });
|
|
262
|
+
} catch (error) {
|
|
263
|
+
warn(`the paired relay being removed did not take the ends: ${describeFailure(entry, error)}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
relayEvents.noteRelayRemoved(relayId);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Boot: fetch from every paired relay this relay holds subscriptions on. */
|
|
270
|
+
function start() {
|
|
271
|
+
if (started) return;
|
|
272
|
+
started = true;
|
|
273
|
+
let delay = 0;
|
|
274
|
+
for (const relayId of relayEvents.remoteRelaysToPoll()) {
|
|
275
|
+
schedule(relayId, delay);
|
|
276
|
+
delay += 250;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function stop() {
|
|
281
|
+
started = false;
|
|
282
|
+
for (const link of links.values()) {
|
|
283
|
+
if (link.timer) clearTimer(link.timer);
|
|
284
|
+
link.timer = null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return { start, stop, fetchNow, refresh, noteRelayRemoved, isPolling: (relayId) => links.has(toText(relayId)) };
|
|
289
|
+
}
|