@sigx/lynx-websocket 0.1.0
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/LICENSE +21 -0
- package/README.md +147 -0
- package/android/com/sigx/websocket/WebSocketEventBus.kt +104 -0
- package/android/com/sigx/websocket/WebSocketModule.kt +78 -0
- package/android/com/sigx/websocket/WebSocketPublisher.kt +44 -0
- package/android/com/sigx/websocket/WebSocketTaskStore.kt +115 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/websocket.d.ts +91 -0
- package/dist/websocket.d.ts.map +1 -0
- package/dist/websocket.js +429 -0
- package/dist/websocket.js.map +1 -0
- package/ios/WebSocketEventBus.swift +86 -0
- package/ios/WebSocketModule.swift +89 -0
- package/ios/WebSocketPublisher.swift +34 -0
- package/ios/WebSocketTaskStore.swift +128 -0
- package/package.json +58 -0
- package/sigx-module.json +19 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Process-wide registry of live `URLSessionWebSocketTask` instances keyed
|
|
4
|
+
/// by the JS-supplied numeric id. Sockets outlive any single LynxView (a
|
|
5
|
+
/// page reload should not drop in-flight sockets that the JS bundle is
|
|
6
|
+
/// re-attaching to), and the underlying `URLSession` is held here so its
|
|
7
|
+
/// delegate callbacks have a stable target.
|
|
8
|
+
final class WebSocketTaskStore: NSObject, URLSessionWebSocketDelegate {
|
|
9
|
+
|
|
10
|
+
static let shared = WebSocketTaskStore()
|
|
11
|
+
|
|
12
|
+
private let queue = DispatchQueue(label: "com.sigx.websocket.store")
|
|
13
|
+
private var tasks: [Int: URLSessionWebSocketTask] = [:]
|
|
14
|
+
/// Reverse lookup so delegate methods (which receive the task, not the
|
|
15
|
+
/// id) can find the id.
|
|
16
|
+
private var idsByTask: [ObjectIdentifier: Int] = [:]
|
|
17
|
+
|
|
18
|
+
/// One shared session — URLSession multiplexes WS tasks just fine, and
|
|
19
|
+
/// keeping a single delegate target reduces book-keeping.
|
|
20
|
+
private lazy var session: URLSession = {
|
|
21
|
+
let cfg = URLSessionConfiguration.default
|
|
22
|
+
// No timeout for established sockets — close frames drive teardown.
|
|
23
|
+
cfg.timeoutIntervalForRequest = 60
|
|
24
|
+
cfg.timeoutIntervalForResource = 0
|
|
25
|
+
return URLSession(configuration: cfg, delegate: self, delegateQueue: nil)
|
|
26
|
+
}()
|
|
27
|
+
|
|
28
|
+
func create(id: Int, request: URLRequest) {
|
|
29
|
+
queue.sync {
|
|
30
|
+
// If JS re-uses an id (it shouldn't — ids are monotonic), tear
|
|
31
|
+
// down the prior task first so we don't leak.
|
|
32
|
+
if let old = tasks[id] {
|
|
33
|
+
old.cancel()
|
|
34
|
+
idsByTask.removeValue(forKey: ObjectIdentifier(old))
|
|
35
|
+
}
|
|
36
|
+
let task = session.webSocketTask(with: request)
|
|
37
|
+
tasks[id] = task
|
|
38
|
+
idsByTask[ObjectIdentifier(task)] = id
|
|
39
|
+
task.resume()
|
|
40
|
+
pump(id: id, task: task)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func task(forId id: Int) -> URLSessionWebSocketTask? {
|
|
45
|
+
queue.sync { tasks[id] }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func close(id: Int, code: Int, reason: String) {
|
|
49
|
+
queue.sync {
|
|
50
|
+
guard let task = tasks[id] else { return }
|
|
51
|
+
let closeCode = URLSessionWebSocketTask.CloseCode(rawValue: code) ?? .normalClosure
|
|
52
|
+
let reasonData = reason.data(using: .utf8)
|
|
53
|
+
task.cancel(with: closeCode, reason: reasonData)
|
|
54
|
+
// Cleanup happens in `urlSession(_:webSocketTask:didCloseWith:)`.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// MARK: - URLSessionWebSocketDelegate
|
|
59
|
+
|
|
60
|
+
func urlSession(_ session: URLSession,
|
|
61
|
+
webSocketTask: URLSessionWebSocketTask,
|
|
62
|
+
didOpenWithProtocol protocolStr: String?) {
|
|
63
|
+
guard let id = queue.sync(execute: { idsByTask[ObjectIdentifier(webSocketTask)] }) else { return }
|
|
64
|
+
WebSocketEventBus.shared.publish(
|
|
65
|
+
open: protocolStr ?? "",
|
|
66
|
+
extensions: "",
|
|
67
|
+
id: id
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func urlSession(_ session: URLSession,
|
|
72
|
+
webSocketTask: URLSessionWebSocketTask,
|
|
73
|
+
didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
|
|
74
|
+
reason: Data?) {
|
|
75
|
+
let id: Int? = queue.sync {
|
|
76
|
+
let key = ObjectIdentifier(webSocketTask)
|
|
77
|
+
let v = idsByTask[key]
|
|
78
|
+
idsByTask.removeValue(forKey: key)
|
|
79
|
+
if let v = v { tasks.removeValue(forKey: v) }
|
|
80
|
+
return v
|
|
81
|
+
}
|
|
82
|
+
guard let id = id else { return }
|
|
83
|
+
let reasonStr = reason.flatMap { String(data: $0, encoding: .utf8) } ?? ""
|
|
84
|
+
WebSocketEventBus.shared.publish(
|
|
85
|
+
close: closeCode.rawValue,
|
|
86
|
+
reason: reasonStr,
|
|
87
|
+
wasClean: closeCode != .abnormalClosure,
|
|
88
|
+
id: id
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// URLSessionWebSocketTask uses `receive(completionHandler:)` for reads —
|
|
93
|
+
// we have to pump it ourselves and re-arm after each message. The first
|
|
94
|
+
// call is kicked off in `create(id:request:)`.
|
|
95
|
+
private func pump(id: Int, task: URLSessionWebSocketTask) {
|
|
96
|
+
task.receive { [weak self] result in
|
|
97
|
+
switch result {
|
|
98
|
+
case .failure(let error):
|
|
99
|
+
// Either the socket closed cleanly (didCloseWith fires
|
|
100
|
+
// separately) or it failed mid-stream — surface the latter
|
|
101
|
+
// as an error event. Cleanup is driven by didCloseWith.
|
|
102
|
+
let nsErr = error as NSError
|
|
103
|
+
if nsErr.code != NSURLErrorCancelled {
|
|
104
|
+
WebSocketEventBus.shared.publish(error: error.localizedDescription, id: id)
|
|
105
|
+
// didCloseWith may not fire for transport errors — synthesize.
|
|
106
|
+
WebSocketEventBus.shared.publish(close: 1006, reason: error.localizedDescription, wasClean: false, id: id)
|
|
107
|
+
self?.queue.sync {
|
|
108
|
+
if let task = self?.tasks.removeValue(forKey: id) {
|
|
109
|
+
self?.idsByTask.removeValue(forKey: ObjectIdentifier(task))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return
|
|
114
|
+
case .success(let message):
|
|
115
|
+
switch message {
|
|
116
|
+
case .string(let s):
|
|
117
|
+
WebSocketEventBus.shared.publish(messageText: s, id: id)
|
|
118
|
+
case .data(let d):
|
|
119
|
+
WebSocketEventBus.shared.publish(messageBinary: d.base64EncodedString(), id: id)
|
|
120
|
+
@unknown default:
|
|
121
|
+
break
|
|
122
|
+
}
|
|
123
|
+
// Re-arm.
|
|
124
|
+
self?.pump(id: id, task: task)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sigx/lynx-websocket",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browser-standard WebSocket client for sigx-lynx",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./sigx-module.json": "./sigx-module.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"ios",
|
|
18
|
+
"android",
|
|
19
|
+
"sigx-module.json"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@sigx/lynx-core": "^0.1.2"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"typescript": "^6.0.3",
|
|
26
|
+
"vitest": "^4.1.6"
|
|
27
|
+
},
|
|
28
|
+
"author": "Andreas Ekdahl",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/signalxjs/lynx.git",
|
|
33
|
+
"directory": "packages/lynx-websocket"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/signalxjs/lynx/tree/main/packages/lynx-websocket",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/signalxjs/lynx/issues"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"signalx",
|
|
44
|
+
"sigx",
|
|
45
|
+
"lynx",
|
|
46
|
+
"mobile",
|
|
47
|
+
"ios",
|
|
48
|
+
"android",
|
|
49
|
+
"websocket",
|
|
50
|
+
"ws",
|
|
51
|
+
"realtime"
|
|
52
|
+
],
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsc",
|
|
55
|
+
"dev": "tsc --watch",
|
|
56
|
+
"test": "vitest run"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/sigx-module.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "WebSocket",
|
|
3
|
+
"package": "@sigx/lynx-websocket",
|
|
4
|
+
"description": "Browser-standard WebSocket client",
|
|
5
|
+
"platforms": ["android", "ios"],
|
|
6
|
+
"ios": {
|
|
7
|
+
"moduleClass": "WebSocketModule",
|
|
8
|
+
"publisherClass": "WebSocketPublisher",
|
|
9
|
+
"sourceDir": "ios",
|
|
10
|
+
"methods": ["create", "send", "close"]
|
|
11
|
+
},
|
|
12
|
+
"android": {
|
|
13
|
+
"moduleClass": "com.sigx.websocket.WebSocketModule",
|
|
14
|
+
"publisherClass": "com.sigx.websocket.WebSocketPublisher",
|
|
15
|
+
"sourceDir": "android",
|
|
16
|
+
"dependencies": ["com.squareup.okhttp3:okhttp:4.12.0"],
|
|
17
|
+
"permissions": []
|
|
18
|
+
}
|
|
19
|
+
}
|