libp2r2p 0.11.14 → 0.11.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -2
- package/package.json +1 -1
- package/relay/helpers/read-admission.js +1 -1
- package/relay/helpers/read-retry.js +106 -0
- package/relay/services/relay-pool.js +108 -41
package/README.md
CHANGED
|
@@ -136,12 +136,39 @@ coverage of a disconnected interval. This is client-observed continuity, not a
|
|
|
136
136
|
relay acknowledgement; delayed relay delivery or clock skew beyond the overlap
|
|
137
137
|
can still leave gaps. A backward local clock cannot regress an emitted boundary.
|
|
138
138
|
|
|
139
|
-
Every involuntary live close emits an error
|
|
139
|
+
Every involuntary live close emits an error. Recoverable failures permit automatic recovery.
|
|
140
140
|
A close without an explicit cause uses `error.code === 'RELAY_LIVE_INTERRUPTED'`
|
|
141
141
|
and `category === 'transport'`; a pre-EOSE close retains status `closed` in the
|
|
142
142
|
initial report. Explicit causes are preserved. Caller cancellation, draining and
|
|
143
143
|
expiry of `filter.until` do not manufacture interruption errors.
|
|
144
144
|
|
|
145
|
+
### Failure-aware live recovery (0.11.15)
|
|
146
|
+
|
|
147
|
+
Live readers retry connection/transport/timeouts and structured `rate-limited:`
|
|
148
|
+
or `error:` refusals. Policy refusals (`blocked:`, `restricted:`, `auth-required:`,
|
|
149
|
+
`pow:`), local authentication/validation failures and unknown errors stop that
|
|
150
|
+
relay for this iterator. Other relays continue. When every route has stopped,
|
|
151
|
+
the iterator finishes after draining accepted events and original diagnostics;
|
|
152
|
+
its readiness snapshot never claims a failed route sent EOSE. A new explicit
|
|
153
|
+
reader starts a new operation; refusals do not blacklist a relay across readers.
|
|
154
|
+
|
|
155
|
+
Retry delays are 1, 2, 4... seconds, capped at five minutes without an attempt
|
|
156
|
+
limit. The next attempt waits until max(backoff deadline, relay retryAt). A
|
|
157
|
+
confirmed offline failure spends no backoff step: it waits for the existing
|
|
158
|
+
shared `onOnline` monitor, without holding read-admission slots. Connectivity
|
|
159
|
+
checks are bounded and shared per pool; structured relay responses need no new
|
|
160
|
+
internet probe unless the browser indicates offline. Cancelled readers release
|
|
161
|
+
their own waits without cancelling another reader's recovery.
|
|
162
|
+
|
|
163
|
+
Successful live EOSE and any required reconnect history reset the delay.
|
|
164
|
+
Transient reconnect-history failures retry while retaining their original gap
|
|
165
|
+
baseline; definitive failures stop that route without certifying its gap.
|
|
166
|
+
Accepted history and buffered live remain deliverable. Local queue-full,
|
|
167
|
+
queue-timeout and disconnect admission failures retain local recovery; impossible
|
|
168
|
+
capacity is terminal. Caller cancellation is silent. Native timeout messages stay
|
|
169
|
+
unchanged and now consistently carry category `timeout`. Constructor hooks
|
|
170
|
+
`_isOnline`/`_onOnline` are internal test injections, not an app bridge API.
|
|
171
|
+
|
|
145
172
|
Process Nostr events only under `item.type === 'event'`. Handle controls you need
|
|
146
173
|
and ignore unknown types; a new control is neither an event nor stream completion.
|
|
147
174
|
The internal query, private-channel and NIP-46 consumers retain their existing
|
|
@@ -161,7 +188,7 @@ atomically before either REQ opens; completing history releases only its slot.
|
|
|
161
188
|
One-shot queries and reconnect gap reads share this budget. Reconnection reserves
|
|
162
189
|
live+history together when recovery is needed. Caller cancellation removes queued
|
|
163
190
|
work; `disconnect(url)` also rejects queued work for that connection. Existing
|
|
164
|
-
live streams retain
|
|
191
|
+
live streams retain recovery for eligible failures until cancelled by their owner.
|
|
165
192
|
|
|
166
193
|
All event reads accept `queueTimeout: 30000` (milliseconds, `null` disables).
|
|
167
194
|
The network `timeout` starts **after admission for each relay**, including its
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@ import { ValidationError } from '../../error/index.js'
|
|
|
2
2
|
import { maybeUnref } from './timer.js'
|
|
3
3
|
|
|
4
4
|
export function admissionError (code, relay) {
|
|
5
|
-
return Object.assign(new Error(code), { code, relay, phase: 'admission' })
|
|
5
|
+
return Object.assign(new Error(code), { code, relay, phase: 'admission', ...(code === 'RELAY_READ_QUEUE_TIMEOUT' ? { category: 'timeout' } : {}) })
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
// One FIFO per pooled connection. A feed reserves both REQs atomically; its
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { isOnline, onOnline } from '../../network/index.js'
|
|
2
|
+
import { isRetryableRelayFailure } from './failure.js'
|
|
3
|
+
import { maybeUnref } from './timer.js'
|
|
4
|
+
|
|
5
|
+
const LOCAL_RETRY_CODES = new Set(['RELAY_READ_QUEUE_FULL', 'RELAY_READ_QUEUE_TIMEOUT', 'RELAY_DISCONNECTED'])
|
|
6
|
+
|
|
7
|
+
export function isRetryableReadFailure (error) {
|
|
8
|
+
if (error?.name === 'ValidationError' || error?.name === 'Nip42AuthenticationError') return false
|
|
9
|
+
if (error?.phase === 'admission') return LOCAL_RETRY_CODES.has(error.code)
|
|
10
|
+
return isRetryableRelayFailure(error)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function needsConnectivityCheck (error) {
|
|
14
|
+
return globalThis.navigator?.onLine === false || (error?.phase !== 'admission' && ['connection', 'transport', 'timeout'].includes(error?.category))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function waitUntil (deadline, signal) {
|
|
18
|
+
signal.throwIfAborted()
|
|
19
|
+
if (deadline <= Date.now()) return Promise.resolve()
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const finish = () => { clearTimeout(timer); signal.removeEventListener('abort', abort); resolve() }
|
|
22
|
+
const abort = () => { clearTimeout(timer); signal.removeEventListener('abort', abort); reject(signal.reason) }
|
|
23
|
+
const timer = maybeUnref(setTimeout(finish, Math.max(0, deadline - Date.now())))
|
|
24
|
+
signal.addEventListener('abort', abort, { once: true })
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function waitFor (promise, signal) {
|
|
29
|
+
signal.throwIfAborted()
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const abort = () => { signal.removeEventListener('abort', abort); reject(signal.reason) }
|
|
32
|
+
signal.addEventListener('abort', abort, { once: true })
|
|
33
|
+
promise.then(value => { signal.removeEventListener('abort', abort); resolve(value) }, error => {
|
|
34
|
+
signal.removeEventListener('abort', abort); reject(error)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// One connectivity check/online subscription per pool, with independent waits.
|
|
40
|
+
// Ending one reader must never cancel recovery still needed by another reader.
|
|
41
|
+
export class ReadRetry {
|
|
42
|
+
#checkOnline
|
|
43
|
+
#watchOnline
|
|
44
|
+
#probe
|
|
45
|
+
#monitor
|
|
46
|
+
#waiters = new Set()
|
|
47
|
+
|
|
48
|
+
constructor ({ checkOnline = isOnline, watchOnline = onOnline } = {}) {
|
|
49
|
+
this.#checkOnline = checkOnline
|
|
50
|
+
this.#watchOnline = watchOnline
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async confirmOnline (signal) {
|
|
54
|
+
signal.throwIfAborted()
|
|
55
|
+
if (globalThis.navigator?.onLine === false) return false
|
|
56
|
+
if (!this.#probe || this.#probe.controller.signal.aborted) {
|
|
57
|
+
const controller = new AbortController()
|
|
58
|
+
const record = { controller, users: 0, finished: false }
|
|
59
|
+
const stopped = new Promise(resolve => controller.signal.addEventListener('abort', () => resolve(false), { once: true }))
|
|
60
|
+
const timer = maybeUnref(setTimeout(() => controller.abort(), 6000))
|
|
61
|
+
record.promise = Promise.race([
|
|
62
|
+
Promise.resolve().then(() => this.#checkOnline({ signal: controller.signal })).catch(() => false), stopped
|
|
63
|
+
]).then(value => value === true).finally(() => {
|
|
64
|
+
record.finished = true
|
|
65
|
+
clearTimeout(timer)
|
|
66
|
+
controller.abort()
|
|
67
|
+
if (this.#probe === record) this.#probe = null
|
|
68
|
+
})
|
|
69
|
+
this.#probe = record
|
|
70
|
+
}
|
|
71
|
+
const record = this.#probe
|
|
72
|
+
record.users++
|
|
73
|
+
try { return await waitFor(record.promise, signal) } finally {
|
|
74
|
+
if (--record.users === 0 && !record.finished) record.controller.abort()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
waitUntilOnline (signal) {
|
|
79
|
+
signal.throwIfAborted()
|
|
80
|
+
const ready = Promise.withResolvers()
|
|
81
|
+
this.#waiters.add(ready)
|
|
82
|
+
if (!this.#monitor) {
|
|
83
|
+
const monitor = { stop: () => {}, ended: false }
|
|
84
|
+
this.#monitor = monitor
|
|
85
|
+
monitor.stop = this.#watchOnline(() => {
|
|
86
|
+
if (this.#monitor !== monitor) return
|
|
87
|
+
for (const waiter of this.#waiters) waiter.resolve()
|
|
88
|
+
this.#waiters.clear()
|
|
89
|
+
this.#stopIfIdle()
|
|
90
|
+
})
|
|
91
|
+
if (monitor.ended) monitor.stop()
|
|
92
|
+
}
|
|
93
|
+
return waitFor(ready.promise, signal).finally(() => {
|
|
94
|
+
this.#waiters.delete(ready)
|
|
95
|
+
this.#stopIfIdle()
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#stopIfIdle () {
|
|
100
|
+
if (this.#waiters.size || !this.#monitor) return
|
|
101
|
+
const monitor = this.#monitor
|
|
102
|
+
this.#monitor = null
|
|
103
|
+
monitor.ended = true
|
|
104
|
+
monitor.stop()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -5,7 +5,8 @@ import { createPublishSettlements, firstFulfillment, publishSummary } from '../h
|
|
|
5
5
|
import { ReadAdmission } from '../helpers/read-admission.js'
|
|
6
6
|
import { drainableStream } from '../helpers/drainable-stream.js'
|
|
7
7
|
import { maybeUnref } from '../helpers/timer.js'
|
|
8
|
-
import { categorizeRelayError } from '../helpers/error.js'
|
|
8
|
+
import { categorizeRelayError, relayTimeoutError } from '../helpers/error.js'
|
|
9
|
+
import { ReadRetry, isRetryableReadFailure, needsConnectivityCheck, waitUntil } from '../helpers/read-retry.js'
|
|
9
10
|
import { normalizeRelayUrl } from '../../url/index.js'
|
|
10
11
|
import { RelayConnection } from './relay-connection.js'
|
|
11
12
|
|
|
@@ -76,11 +77,11 @@ function countResponseError () {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
function countTimeoutError () {
|
|
79
|
-
return
|
|
80
|
+
return relayTimeoutError('COUNT_TIMEOUT')
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
function getEventsTimeoutError () {
|
|
83
|
-
return
|
|
84
|
+
return relayTimeoutError('GET_EVENTS_TIMEOUT')
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
function asError (error) {
|
|
@@ -138,11 +139,13 @@ export class RelayPool {
|
|
|
138
139
|
#createRelay
|
|
139
140
|
#WebSocket
|
|
140
141
|
#admission
|
|
142
|
+
#readRetry
|
|
141
143
|
|
|
142
|
-
constructor ({ _createRelay, WebSocket: WebSocketImpl, ...capacity } = {}) {
|
|
144
|
+
constructor ({ _createRelay, _isOnline, _onOnline, WebSocket: WebSocketImpl, ...capacity } = {}) {
|
|
143
145
|
this.#WebSocket = WebSocketImpl
|
|
144
146
|
this.#createRelay = _createRelay ?? (url => new RelayConnection(url, this.#WebSocket ? { WebSocket: this.#WebSocket } : undefined))
|
|
145
147
|
this.#admission = new ReadAdmission(capacity)
|
|
148
|
+
this.#readRetry = new ReadRetry({ checkOnline: _isOnline, watchOnline: _onOnline })
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
// Injects the WebSocket implementation used by new pooled connections (e.g.
|
|
@@ -600,6 +603,9 @@ export class RelayPool {
|
|
|
600
603
|
const gapTasks = new Set()
|
|
601
604
|
const liveSubs = new Map() // url → live sub
|
|
602
605
|
const retryTimers = new Map()
|
|
606
|
+
const retryDelays = new Map()
|
|
607
|
+
const stoppedRelays = new Set()
|
|
608
|
+
const retainedRelays = new Set()
|
|
603
609
|
const initialPending = new Set(urls)
|
|
604
610
|
const initialOutcomes = new Map()
|
|
605
611
|
const readyTimeouts = new Map()
|
|
@@ -708,7 +714,7 @@ export class RelayPool {
|
|
|
708
714
|
clearTimeout(attempt.timer)
|
|
709
715
|
attempt.stop.abort()
|
|
710
716
|
}
|
|
711
|
-
for (const
|
|
717
|
+
for (const retry of retryTimers.values()) retry.abort()
|
|
712
718
|
retryTimers.clear()
|
|
713
719
|
liveSubs.forEach(sub => sub.close())
|
|
714
720
|
liveSubs.clear()
|
|
@@ -768,14 +774,61 @@ export class RelayPool {
|
|
|
768
774
|
}
|
|
769
775
|
}
|
|
770
776
|
|
|
771
|
-
const
|
|
772
|
-
if (
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
777
|
+
const releaseRelay = url => {
|
|
778
|
+
if (retainedRelays.delete(url)) this.#decrementLiveSub(url)
|
|
779
|
+
}
|
|
780
|
+
const closeAttempt = (url, attempt) => {
|
|
781
|
+
if (!attempt || attempt.closed) return
|
|
782
|
+
attempt.closed = true
|
|
783
|
+
clearTimeout(attempt.timer)
|
|
784
|
+
attempt.stop.abort()
|
|
785
|
+
const sub = liveSubs.get(url)
|
|
786
|
+
liveSubs.delete(url)
|
|
787
|
+
sub?.close()
|
|
788
|
+
liveLeases.get(url)?.release()
|
|
789
|
+
liveLeases.delete(url)
|
|
790
|
+
readyRelays.delete(url)
|
|
791
|
+
}
|
|
792
|
+
const stopRelay = url => {
|
|
793
|
+
stoppedRelays.add(url)
|
|
794
|
+
retryTimers.get(url)?.abort()
|
|
795
|
+
retryTimers.delete(url)
|
|
796
|
+
closeAttempt(url, attempts.get(url))
|
|
797
|
+
releaseRelay(url)
|
|
798
|
+
if (stoppedRelays.size === urls.length) { finishReady(); teardown(true) }
|
|
799
|
+
}
|
|
800
|
+
const scheduleReconnect = (url, error) => {
|
|
801
|
+
if (isDone || stoppedRelays.has(url) || retryTimers.has(url)) return
|
|
802
|
+
if (!isRetryableReadFailure(error)) { stopRelay(url); return }
|
|
803
|
+
const retry = new AbortController()
|
|
804
|
+
const retrySignal = AbortSignal.any([gapAc.signal, retry.signal])
|
|
805
|
+
const failedAt = Date.now()
|
|
806
|
+
const delay = retryDelays.get(url) ?? 1000
|
|
807
|
+
const retryAt = Number.isFinite(error?.retryAt)
|
|
808
|
+
? error.retryAt
|
|
809
|
+
: Number.isFinite(error?.retryAfterMs) && error.retryAfterMs > 0 ? failedAt + Math.min(error.retryAfterMs, 300000) : 0
|
|
810
|
+
retryTimers.set(url, retry)
|
|
811
|
+
;(async () => {
|
|
812
|
+
const online = !needsConnectivityCheck(error) || await this.#readRetry.confirmOnline(retrySignal)
|
|
813
|
+
retrySignal.throwIfAborted()
|
|
814
|
+
const deadline = Math.max(failedAt + (online ? delay : 0), retryAt)
|
|
815
|
+
if (online) retryDelays.set(url, Math.min(delay * 2, 300000))
|
|
816
|
+
else await this.#readRetry.waitUntilOnline(retrySignal)
|
|
817
|
+
while (!retrySignal.aborted) {
|
|
818
|
+
await waitUntil(deadline, retrySignal)
|
|
819
|
+
if (needsConnectivityCheck(error) && !await this.#readRetry.confirmOnline(retrySignal)) {
|
|
820
|
+
await this.#readRetry.waitUntilOnline(retrySignal)
|
|
821
|
+
continue
|
|
822
|
+
}
|
|
823
|
+
retrySignal.throwIfAborted()
|
|
824
|
+
if (isDone || stoppedRelays.has(url) || retryTimers.get(url) !== retry) return
|
|
825
|
+
retryTimers.delete(url)
|
|
826
|
+
subscribeToRelay(url, pendingGaps.get(url) ?? lastSeenAt.get(url) ?? openedAt.get(url) ?? null)
|
|
827
|
+
return
|
|
828
|
+
}
|
|
829
|
+
})().catch(failure => {
|
|
830
|
+
if (!retrySignal.aborted && !isDone) { reportFailure(url, asError(failure)); stopRelay(url) }
|
|
831
|
+
})
|
|
779
832
|
}
|
|
780
833
|
|
|
781
834
|
// Schedule teardown when the wall clock reaches filter.until
|
|
@@ -799,26 +852,36 @@ export class RelayPool {
|
|
|
799
852
|
})
|
|
800
853
|
return (async () => {
|
|
801
854
|
let completed = false
|
|
802
|
-
let
|
|
855
|
+
let error
|
|
803
856
|
for await (const item of gapGen) {
|
|
804
857
|
if (item?.type === 'event') {
|
|
805
858
|
observeEvent(item.event, url)
|
|
806
859
|
pushEvent(item.event, url, true)
|
|
807
860
|
} else if (item?.type === 'error') {
|
|
808
|
-
|
|
861
|
+
if (!error || !isRetryableReadFailure(item.error)) error = item.error
|
|
809
862
|
if (!isDone) enqueue(item)
|
|
810
863
|
} else if (item?.type === 'eose') {
|
|
811
864
|
completed = item.relays?.length === 1 && ['eose', 'satisfied'].includes(item.relays[0].status)
|
|
865
|
+
if (!completed && !error) {
|
|
866
|
+
error = item.relays?.[0]?.error || categorizeRelayError(new Error('RELAY_RECONNECT_GAP_INCOMPLETE'), 'timeout')
|
|
867
|
+
if (!isDone) enqueue({ type: 'error', relay: url, error })
|
|
868
|
+
}
|
|
812
869
|
}
|
|
813
870
|
}
|
|
814
|
-
|
|
871
|
+
if (!completed && !error && !attempt.stop.signal.aborted && !isDone) {
|
|
872
|
+
error = new Error('RELAY_RECONNECT_GAP_INCOMPLETE')
|
|
873
|
+
reportFailure(url, error)
|
|
874
|
+
}
|
|
875
|
+
return { recovered: completed && !error, error }
|
|
815
876
|
})().catch(err => {
|
|
816
|
-
|
|
817
|
-
|
|
877
|
+
const error = asError(err)
|
|
878
|
+
if (!attempt.stop.signal.aborted) reportFailure(url, error)
|
|
879
|
+
return { recovered: false, error }
|
|
818
880
|
}).finally(() => slots.history.release())
|
|
819
881
|
}
|
|
820
882
|
|
|
821
|
-
const subscribeToRelay = (url, gapSince
|
|
883
|
+
const subscribeToRelay = (url, gapSince) => {
|
|
884
|
+
if (isDone || stoppedRelays.has(url)) return
|
|
822
885
|
let now = Math.floor(Date.now() / 1000)
|
|
823
886
|
// Don't reconnect if we're past the until boundary
|
|
824
887
|
if (filterUntil !== null && now >= filterUntil) return
|
|
@@ -829,10 +892,10 @@ export class RelayPool {
|
|
|
829
892
|
attempts.set(url, attempt)
|
|
830
893
|
let recoveryLease
|
|
831
894
|
initialAdmissions.add(url)
|
|
832
|
-
Promise.resolve(reservation || this.#admission.acquire(normalizeRelayUrl(url), { live: true, history: recovering, signal: gapAc.signal, queueTimeout })).then(async slots => {
|
|
895
|
+
Promise.resolve(reservation || this.#admission.acquire(normalizeRelayUrl(url), { live: true, history: recovering, signal: AbortSignal.any([gapAc.signal, attempt.stop.signal]), queueTimeout })).then(async slots => {
|
|
833
896
|
const lease = slots.live
|
|
834
897
|
recoveryLease = recovering ? slots.history : null
|
|
835
|
-
if (isDone) { lease.release(); recoveryLease?.release(); return }
|
|
898
|
+
if (isDone || attempt.closed) { lease.release(); recoveryLease?.release(); return }
|
|
836
899
|
liveLeases.set(url, lease)
|
|
837
900
|
if (timeout !== null && initialPending.has(url)) {
|
|
838
901
|
readyTimeouts.set(url, maybeUnref(setTimeout(() => {
|
|
@@ -844,7 +907,7 @@ export class RelayPool {
|
|
|
844
907
|
}, timeout)))
|
|
845
908
|
}
|
|
846
909
|
const relay = await this.#getRelay(url)
|
|
847
|
-
if (isDone) { recoveryLease?.release(); return }
|
|
910
|
+
if (isDone || attempt.closed) { lease.release(); recoveryLease?.release(); return }
|
|
848
911
|
now = Math.floor(Date.now() / 1000)
|
|
849
912
|
if (filterUntil !== null && now >= filterUntil) { lease.release(); recoveryLease?.release(); return }
|
|
850
913
|
|
|
@@ -878,37 +941,39 @@ export class RelayPool {
|
|
|
878
941
|
},
|
|
879
942
|
onclose: error => {
|
|
880
943
|
if (attempt.closed || attempts.get(url) !== attempt) return
|
|
881
|
-
attempt
|
|
882
|
-
clearTimeout(attempt.timer)
|
|
883
|
-
attempt.stop.abort()
|
|
884
|
-
if (liveSubs.get(url) === liveSub) liveSubs.delete(url)
|
|
885
|
-
lease.release()
|
|
886
|
-
if (liveLeases.get(url) === lease) liveLeases.delete(url)
|
|
887
|
-
readyRelays.delete(url)
|
|
944
|
+
closeAttempt(url, attempt)
|
|
888
945
|
if (isDone) return
|
|
889
|
-
if (error !== undefined) reportFailure(url,
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
reportClosed(url, interruption)
|
|
946
|
+
if (error !== undefined) { error = asError(error); reportFailure(url, error) } else {
|
|
947
|
+
error = Object.assign(categorizeRelayError(new Error('RELAY_LIVE_INTERRUPTED'), 'transport'), { code: 'RELAY_LIVE_INTERRUPTED' })
|
|
948
|
+
enqueue({ type: 'error', relay: url, error })
|
|
949
|
+
reportClosed(url, error)
|
|
894
950
|
}
|
|
895
|
-
scheduleReconnect(url,
|
|
951
|
+
scheduleReconnect(url, error)
|
|
896
952
|
},
|
|
897
953
|
oneose: () => {
|
|
898
954
|
if (isDone || attempt.closed || attempts.get(url) !== attempt || liveEose) return
|
|
899
955
|
liveEose = true
|
|
900
956
|
attempt.since = Math.max(sinceFloor, Math.floor(Date.now() / 1000))
|
|
901
957
|
markInitialEose(url)
|
|
958
|
+
if (attempt.recovered) retryDelays.delete(url)
|
|
902
959
|
scheduleProgress(attempt)
|
|
903
960
|
}
|
|
904
961
|
})
|
|
905
|
-
if (isDone) { liveSub.close(); recoveryLease?.release(); return }
|
|
962
|
+
if (isDone || attempt.closed) { liveSub.close(); lease.release(); recoveryLease?.release(); return }
|
|
906
963
|
liveSubs.set(url, liveSub)
|
|
907
964
|
|
|
908
965
|
if (recovering) {
|
|
909
|
-
const task = runReconnectGapFill(url, gapSince, now, slots, attempt).then(recovered => {
|
|
966
|
+
const task = runReconnectGapFill(url, gapSince, now, slots, attempt).then(({ recovered, error }) => {
|
|
910
967
|
attempt.recovered = recovered
|
|
911
|
-
if (
|
|
968
|
+
if (!attempt.closed && attempts.get(url) === attempt) {
|
|
969
|
+
if (recovered) {
|
|
970
|
+
pendingGaps.delete(url)
|
|
971
|
+
if (liveEose) retryDelays.delete(url)
|
|
972
|
+
} else if (error && !isDone) {
|
|
973
|
+
closeAttempt(url, attempt)
|
|
974
|
+
scheduleReconnect(url, error)
|
|
975
|
+
}
|
|
976
|
+
}
|
|
912
977
|
}).finally(() => {
|
|
913
978
|
const buf = liveBuffer
|
|
914
979
|
liveBuffer = null
|
|
@@ -925,9 +990,10 @@ export class RelayPool {
|
|
|
925
990
|
liveLeases.delete(url)
|
|
926
991
|
readyRelays.delete(url)
|
|
927
992
|
const reason = err instanceof Error ? err : new Error(String(err))
|
|
993
|
+
if (isDone || attempt.closed) return
|
|
994
|
+
closeAttempt(url, attempt)
|
|
928
995
|
reportFailure(url, reason)
|
|
929
|
-
|
|
930
|
-
scheduleReconnect(url, reconnectDelay)
|
|
996
|
+
scheduleReconnect(url, reason)
|
|
931
997
|
})
|
|
932
998
|
}
|
|
933
999
|
|
|
@@ -946,6 +1012,7 @@ export class RelayPool {
|
|
|
946
1012
|
|
|
947
1013
|
for (const url of urls) {
|
|
948
1014
|
this.#incrementLiveSub(url)
|
|
1015
|
+
retainedRelays.add(url)
|
|
949
1016
|
subscribeToRelay(url, null) // no initial gap fill — that's getEventsFeedGenerator's job
|
|
950
1017
|
}
|
|
951
1018
|
|
|
@@ -960,7 +1027,7 @@ export class RelayPool {
|
|
|
960
1027
|
} finally {
|
|
961
1028
|
signal?.removeEventListener('abort', abort)
|
|
962
1029
|
_stopSignal?.removeEventListener('abort', stop)
|
|
963
|
-
for (const url of
|
|
1030
|
+
for (const url of [...retainedRelays]) releaseRelay(url)
|
|
964
1031
|
teardown()
|
|
965
1032
|
}
|
|
966
1033
|
}
|