pusher / pusher/pusher-websocket-react-native

PusherWebsocketReactNative.swift ( IOS Application crash)

Open
#143 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
76
Forks
64
PR merge metrics
No merged PRs in 30d

Description

I'm getting an crash issue in IOS while pusher unsubscribing

PusherWebsocketReactNative.swift - Line 7
@objc PusherWebsocketReactNative.unsubscribe(_:resolve:reject:) + 7

Here's my PusherWebsocketReactNative.swift code:

import PusherSwift
import Foundation

@objc(PusherWebsocketReactNative)
@objcMembers class PusherWebsocketReactNative: RCTEventEmitter, PusherDelegate, Authorizer {
private static var shared: PusherWebsocketReactNative!
private static var pusher: Pusher!

private var authorizerCompletionHandlers = [String: ([String:String]) -> Void]()
private var authorizerCompletionHandlerTimeout = 10 // seconds

private let subscriptionErrorType = "SubscriptionError"
private let authErrorType = "AuthError"
private let pusherEventPrefix = "PusherReactNative"

override init() {
    super.init()

    PusherWebsocketReactNative.shared = self
}

override func supportedEvents() -> [String]! {
    return ["\(pusherEventPrefix):onConnectionStateChange",
            "\(pusherEventPrefix):onSubscriptionError",
            "\(pusherEventPrefix):onSubscriptionCount",
            "\(pusherEventPrefix):onAuthorizer",
            "\(pusherEventPrefix):onError",
            "\(pusherEventPrefix):onDecryptionFailure",
            "\(pusherEventPrefix):onEvent",
            "\(pusherEventPrefix):onMemberAdded",
            "\(pusherEventPrefix):onMemberRemoved"]
}

private func callback(name:String, body:Any) -> Void {
    let pusherEventname = "\(pusherEventPrefix):\(name)"
    PusherWebsocketReactNative.shared.sendEvent(withName:pusherEventname, body:body)
}

func initialize(_ args:[String: Any], resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) {
    if PusherWebsocketReactNative.pusher != nil {
        PusherWebsocketReactNative.pusher.unsubscribeAll()
        PusherWebsocketReactNative.pusher.disconnect()
    }
    var authMethod:AuthMethod = .noMethod
    if args["authEndpoint"] is String {
        authMethod = .endpoint(authEndpoint: args["authEndpoint"] as! String)
    } else if args["authorizer"] is Bool {
        authMethod = .authorizer(authorizer: PusherWebsocketReactNative.shared)
    }
    var host:PusherHost = .defaultHost
    if args["host"] is String {
        host = .host(args["host"] as! String)
    } else if args["cluster"] != nil {
        host = .cluster(args["cluster"] as! String)
    }
    var useTLS:Bool = true
    if args["useTLS"] is Bool {
        useTLS = args["useTLS"] as! Bool
    }
    var port:Int
    if useTLS {
        port = 443
        if args["wssPort"] is Int {
            port = args["wssPort"] as! Int
        }
    } else {
        port = 80
        if args["wsPort"] is Int {
            port = args["wsPort"] as! Int
        }
    }
    var activityTimeout:TimeInterval? = nil
    if args["activityTimeout"] is TimeInterval {
        activityTimeout = args["activityTimeout"] as! Double / 1000.0
    }
    var path:String? = nil
    if args["path"] is String {
        path = (args["path"] as! String)
    }
    let options = PusherClientOptions(
        authMethod: authMethod,
        host: host,
        port: port,
        path: path,
        useTLS: useTLS,
        activityTimeout: activityTimeout
    )
    PusherWebsocketReactNative.pusher = Pusher(key: args["apiKey"] as! String, options: options)
    if args["maxReconnectionAttempts"] is Int {
        PusherWebsocketReactNative.pusher.connection.reconnectAttemptsMax = (args["maxReconnectionAttempts"] as! Int)
    }
    if args["maxReconnectGapInSeconds"] is TimeInterval {
        PusherWebsocketReactNative.pusher.connection.maxReconnectGapInSeconds = (args["maxReconnectGapInSeconds"] as! TimeInterval)
    }
    if args["pongTimeout"] is Int {
        PusherWebsocketReactNative.pusher.connection.pongResponseTimeoutInterval = args["pongTimeout"] as! TimeInterval / 1000.0
    }

    if let authorizerTimeoutInSeconds = args["authorizerTimeoutInSeconds"] as? Int {
        PusherWebsocketReactNative.shared.authorizerCompletionHandlerTimeout = authorizerTimeoutInSeconds
    }

    PusherWebsocketReactNative.pusher.connection.delegate = PusherWebsocketReactNative.shared
    PusherWebsocketReactNative.pusher.bind(eventCallback: onEvent)
    resolve(nil)
}

override static func requiresMainQueueSetup() -> Bool {
    return false
}

public func fetchAuthValue(socketID: String, channelName: String, completionHandler: @escaping (PusherAuth?) -> Void) {
    PusherWebsocketReactNative.shared.callback(name:"onAuthorizer", body: [
        "socketId": socketID,
        "channelName": channelName
    ])

    let key = channelName + socketID
    let authCallback = { (authParams:[String:String]) in
        if let authParam = authParams["auth"] {
            completionHandler(PusherAuth(auth: authParam, channelData: authParams["channel_data"], sharedSecret: authParams["shared_secret"]))
        } else {
            completionHandler(PusherAuth(auth: "<missing_auth_param>:error", channelData: authParams["channel_data"], sharedSecret: authParams["shared_secret"]))
        }
    }
    authorizerCompletionHandlers[key] = authCallback

    // the JS thread might not call onAuthorizer – we need to cleanup the completion handler after timeout
    let timeout = DispatchTimeInterval.seconds(PusherWebsocketReactNative.shared.authorizerCompletionHandlerTimeout)
    DispatchQueue.main.asyncAfter(deadline: .now() + timeout) {
        if let storedAuthHandler = PusherWebsocketReactNative.shared.authorizerCompletionHandlers.removeValue(forKey: key) {
            storedAuthHandler(["auth": "<authorizer_timeout>:error"])
        }
    }
}

public func onAuthorizer(_ channelName: String, socketID: String, data:[String:String], resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) {
    let key = channelName + socketID
    if let storedAuthHandler = authorizerCompletionHandlers.removeValue(forKey: key) {
        storedAuthHandler(data)
    }
}

public func changedConnectionState(from old: ConnectionState, to new: ConnectionState) {
    PusherWebsocketReactNative.shared.callback(name:"onConnectionStateChange", body:[
        "previousState": old.stringValue(),
        "currentState": new.stringValue()
    ])
}

public func debugLog(message: String) {
    //print("DEBUG:", message)
}

public func subscribedToChannel(name: String) {
    // Handled by global handler
}

public func failedToSubscribeToChannel(name: String, response: URLResponse?, data: String?, error: NSError?) {
    var code = ""
    var type = subscriptionErrorType
    if let httpResponse = response as? HTTPURLResponse {
        code = String(httpResponse.statusCode)
        type = authErrorType
    }

    PusherWebsocketReactNative.shared.callback(name:"onSubscriptionError", body:[
        "message": (error != nil) ? error!.localizedDescription : ((data != nil) ? data! : error.debugDescription),
        "type": type,
        "code": code,
        "channelName": name
    ])
}

public func receivedError(error: PusherError) {
    PusherWebsocketReactNative.shared.callback(
        name:"onError", body:[
            "message": error.message,
            "code": error.code ?? -1,
            "error": error.debugDescription
        ]
    )
}

public func failedToDecryptEvent(eventName: String, channelName: String, data: String?) {
    PusherWebsocketReactNative.shared.callback(
        name:"onDecryptionFailure", body:[
            "eventName": eventName,
            "reason": data
        ]
    )
}

public func connect(_ resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) {
    PusherWebsocketReactNative.pusher.connect()
    resolve(nil)
}

public func disconnect(_ resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) {
    if PusherWebsocketReactNative.pusher != nil {
        PusherWebsocketReactNative.pusher.disconnect()
    }
    resolve(nil)
}

public func getSocketId() -> String? {
    return PusherWebsocketReactNative.pusher.connection.socketId
}

func onEvent(event:PusherEvent) {
    var userId:String? = nil
    var mappedEventName:String? = nil
    if event.eventName == "pusher:subscription_succeeded" {
        if let channel = PusherWebsocketReactNative.pusher.connection.channels.findPresence(name: event.channelName!) {
            userId = channel.myId
        }
        mappedEventName = "pusher_internal:subscription_succeeded"
    }
    PusherWebsocketReactNative.shared.callback(
        name:"onEvent",body:[
            "channelName": event.channelName,
            "eventName": mappedEventName ?? event.eventName,
            "userId": event.userId ?? userId,
            "data": event.data
        ]
    )
}

func subscribe(_ channelName:String, resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) {
    if channelName.hasPrefix("presence-") {
        let onMemberAdded:(PusherPresenceChannelMember) -> () = { user in
            PusherWebsocketReactNative.shared.callback(name:"onMemberAdded", body: [
                "channelName": channelName,
                "user": ["userId": user.userId, "userInfo": user.userInfo ]
            ])
        }
        let onMemberRemoved:(PusherPresenceChannelMember) -> () = { user in
            PusherWebsocketReactNative.shared.callback(name:"onMemberRemoved", body: [
                "channelName": channelName,
                "user": ["userId": user.userId, "userInfo": user.userInfo ]
            ])
        }
        PusherWebsocketReactNative.pusher.subscribeToPresenceChannel(
            channelName: channelName,
            onMemberAdded: onMemberAdded,
            onMemberRemoved: onMemberRemoved
        )
    } else {
        let onSubscriptionCount:(Int) -> () = { subscriptionCount in
            PusherWebsocketReactNative.shared.callback(
                name:"onEvent",body:[
                    "channelName": channelName,
                    "eventName": "pusher_internal:subscription_count",
                    "userId": nil,
                    "data": [
                        "subscription_count": subscriptionCount
                    ]
                ]
            )
        }
        PusherWebsocketReactNative.pusher.subscribe(channelName: channelName,
                                                    onSubscriptionCountChanged: onSubscriptionCount)
    }
    resolve(nil)
}

func unsubscribe(_ channelName:String, resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) {
    PusherWebsocketReactNative.pusher.unsubscribe(channelName)
    resolve(nil)
}

func trigger(_ channelName:String, eventName:String, data:Any, resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) {
    if let channel = PusherWebsocketReactNative.pusher.connection.channels.find(name: channelName) {
        channel.trigger(eventName: eventName, data: data)
    }
    resolve(nil)
}
}

and here's our pusherDisconnect code in app.js file


useEffect(() => {
handleGetPusherSetting();
return async () => {
console.log("pusher in return app stack", pusher);
if (pusher !== null && pusher.connectionState !== "DISCONNECTED") {
console.log("inside pusher unsucribe setting")
await pusher.unsubscribe({
channelName: portal-${clientConfigs["app-claims/config-id"]}-${clientConfigs["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"]},
});
await pusher.disconnect();
} else {
const pusher = Pusher.getInstance();
await pusher.unsubscribe({
channelName: portal-${clientConfigs["app-claims/config-id"]}-${clientConfigs["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"]},
});
await pusher.disconnect();
}
};
}, []);

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by inspecting unsubscribe in PusherWebsocketReactNative.swift and the cleanup callback in app.js. Reproduce the iOS crash when the app effect unmounts, and capture the complete crash details rather than only the line reference. Done means the unsubscribe and disconnect cleanup completes without crashing.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, react-native, swift
Domain
mobile
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.