react-nitro-networking 0.0.0-stage → 0.0.1-alpha.7

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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/Package.swift +102 -0
  3. package/README.md +58 -2
  4. package/Sources/ReactNitroNetworking/HybridNetworking.swift +17 -0
  5. package/Sources/ReactNitroNetworking/HybridResponse.swift +87 -0
  6. package/Sources/ReactNitroNetworking/HybridWebSocket.swift +188 -0
  7. package/Sources/ReactNitroNetworking/NetworkingTaskDelegate.swift +16 -0
  8. package/Sources/ReactNitroNetworking/WebSocketListeners.swift +49 -0
  9. package/Sources/ReactNitroNetworking/WebSocketTaskDelegate.swift +26 -0
  10. package/Sources/ReactNitroNetworking/WebSocketURL.swift +18 -0
  11. package/nitrogen/generated/.gitattributes +1 -0
  12. package/nitrogen/generated/apple/ReactNitroNetworking-Swift-Cxx-Bridge.cpp +171 -0
  13. package/nitrogen/generated/apple/ReactNitroNetworkingAppleCxx/ReactNitroNetworkingAppleCxx.cpp +9 -0
  14. package/nitrogen/generated/apple/ReactNitroNetworkingAppleCxx/include/ReactNitroNetworkingAppleCxx/ReactNitroNetworkingAppleCxx.h +97 -0
  15. package/nitrogen/generated/apple/ReactNitroNetworkingAutolinking.cpp +24 -0
  16. package/nitrogen/generated/apple/ReactNitroNetworkingAutolinking.mm +24 -0
  17. package/nitrogen/generated/apple/c++/HybridNetworkingSpecSwift.cpp +42 -0
  18. package/nitrogen/generated/apple/c++/HybridNetworkingSpec_cxx-Swift.cpp +28 -0
  19. package/nitrogen/generated/apple/c++/HybridResponseSpecSwift.cpp +102 -0
  20. package/nitrogen/generated/apple/c++/HybridResponseSpec_cxx-Swift.cpp +28 -0
  21. package/nitrogen/generated/apple/c++/HybridWebSocketSpecSwift.cpp +111 -0
  22. package/nitrogen/generated/apple/c++/HybridWebSocketSpec_cxx-Swift.cpp +28 -0
  23. package/nitrogen/generated/apple/c++/ListenerSubscription-Swift.cpp +37 -0
  24. package/nitrogen/generated/apple/c++/Record_swift__String_swift__String-Swift.cpp +26 -0
  25. package/nitrogen/generated/apple/c++/ResponseType-Swift.cpp +58 -0
  26. package/nitrogen/generated/apple/c++/Variant_ArrayBuffer_String-Swift.cpp +41 -0
  27. package/nitrogen/generated/apple/c++/include/ReactNitroNetworking/ReactNitroNetworkingAutolinking.hpp +13 -0
  28. package/nitrogen/generated/apple/c++/include/ReactNitroNetworking/private/HybridNetworkingSpecSwift.hpp +72 -0
  29. package/nitrogen/generated/apple/c++/include/ReactNitroNetworking/private/HybridNetworkingSpec_cxx-Swift.hpp +23 -0
  30. package/nitrogen/generated/apple/c++/include/ReactNitroNetworking/private/HybridResponseSpecSwift.hpp +81 -0
  31. package/nitrogen/generated/apple/c++/include/ReactNitroNetworking/private/HybridResponseSpec_cxx-Swift.hpp +23 -0
  32. package/nitrogen/generated/apple/c++/include/ReactNitroNetworking/private/HybridWebSocketSpecSwift.hpp +78 -0
  33. package/nitrogen/generated/apple/c++/include/ReactNitroNetworking/private/HybridWebSocketSpec_cxx-Swift.hpp +23 -0
  34. package/nitrogen/generated/apple/c++/include/ReactNitroNetworking/private/ListenerSubscription-Swift.hpp +23 -0
  35. package/nitrogen/generated/apple/c++/include/ReactNitroNetworking/private/ReactNitroNetworking-Swift-Cxx-Umbrella.hpp +44 -0
  36. package/nitrogen/generated/apple/c++/include/ReactNitroNetworking/private/Record_swift__String_swift__String-Swift.hpp +23 -0
  37. package/nitrogen/generated/apple/c++/include/ReactNitroNetworking/private/ResponseType-Swift.hpp +23 -0
  38. package/nitrogen/generated/apple/c++/include/ReactNitroNetworking/private/Variant_ArrayBuffer_String-Swift.hpp +23 -0
  39. package/nitrogen/generated/apple/c++/include/module.modulemap +12 -0
  40. package/nitrogen/generated/apple/swift/Callback_void.swift +60 -0
  41. package/nitrogen/generated/apple/swift/Callback_void___ReactNitroNetworking__Variant_ArrayBuffer_String_cxx.swift +66 -0
  42. package/nitrogen/generated/apple/swift/Callback_void_std__exception_ptr.swift +60 -0
  43. package/nitrogen/generated/apple/swift/HybridNetworkingSpec.swift +65 -0
  44. package/nitrogen/generated/apple/swift/HybridNetworkingSpec_cxx.swift +143 -0
  45. package/nitrogen/generated/apple/swift/HybridResponseSpec.swift +74 -0
  46. package/nitrogen/generated/apple/swift/HybridResponseSpec_cxx.swift +201 -0
  47. package/nitrogen/generated/apple/swift/HybridWebSocketSpec.swift +70 -0
  48. package/nitrogen/generated/apple/swift/HybridWebSocketSpec_cxx.swift +201 -0
  49. package/nitrogen/generated/apple/swift/ListenerSubscription.swift +29 -0
  50. package/nitrogen/generated/apple/swift/Promise_ArrayBuffer.swift +100 -0
  51. package/nitrogen/generated/apple/swift/Promise_NitroModules__AnyJSON.swift +102 -0
  52. package/nitrogen/generated/apple/swift/Promise___ReactNitroNetworking__HybridResponseSpec_cxx.swift +102 -0
  53. package/nitrogen/generated/apple/swift/Promise_swift__String.swift +102 -0
  54. package/nitrogen/generated/apple/swift/Promise_void.swift +100 -0
  55. package/nitrogen/generated/apple/swift/Record_swift__String_swift__String.swift +48 -0
  56. package/nitrogen/generated/apple/swift/ResponseType.swift +74 -0
  57. package/nitrogen/generated/apple/swift/Variant_ArrayBuffer_String.swift +69 -0
  58. package/nitrogen/generated/hybrid-classes.json +29 -0
  59. package/nitrogen/generated/shared/c++/ReactNitroNetworkingCxxModule.cpp +2 -0
  60. package/nitrogen/generated/shared/c++/include/ReactNitroNetworking/ReactNitroNetworkingCxxModule.hpp +2 -0
  61. package/nitrogen/generated/shared/javascript/Networking.cjs +19 -0
  62. package/nitrogen/generated/shared/javascript/Networking.d.cts +9 -0
  63. package/nitrogen/generated/shared/javascript/Response.cjs +19 -0
  64. package/nitrogen/generated/shared/javascript/Response.d.cts +9 -0
  65. package/nitrogen/generated/shared/javascript/WebSocket.cjs +19 -0
  66. package/nitrogen/generated/shared/javascript/WebSocket.d.cts +9 -0
  67. package/package.json +34 -4
  68. package/src/ListenerSubscription.ts +7 -0
  69. package/src/ResponseType.ts +8 -0
  70. package/src/index.ts +5 -0
  71. package/src/specs/Networking.nitro.ts +18 -0
  72. package/src/specs/Response.nitro.ts +37 -0
  73. package/src/specs/WebSocket.nitro.ts +62 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Margelo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9
+ of the Software, and to permit persons to whom the Software is furnished to do
10
+ so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/Package.swift ADDED
@@ -0,0 +1,102 @@
1
+ // swift-tools-version: 6.4
2
+
3
+ import Foundation
4
+ import PackageDescription
5
+
6
+ let name = "ReactNitroNetworking"
7
+ let generated = "nitrogen/generated"
8
+ let nitroDependencies: [Target.Dependency] = [
9
+ .product(name: "NitroModules", package: "nitro-core")
10
+ ]
11
+ let cxxSettings: [CXXSetting] = [
12
+ .define("DEBUG", .when(configuration: .debug)),
13
+ .define("NDEBUG", .when(configuration: .release)),
14
+ .disableWarning("nullability-extension"),
15
+ ]
16
+ let swiftSources = ["Sources/ReactNitroNetworking", "\(generated)/apple/swift"]
17
+ let bridgeSources = [
18
+ "c++", "ReactNitroNetworking-Swift-Cxx-Bridge.cpp", "ReactNitroNetworkingAutolinking.cpp",
19
+ ]
20
+
21
+ // Bare-host integration. Nitrogen generates the bindings; this manifest owns the targets.
22
+ let package = Package(
23
+ name: name,
24
+ platforms: [.iOS(.v16), .macOS(.v13)],
25
+ products: [
26
+ .library(name: name, targets: ["\(name)Cxx", name, "\(name)SwiftBridge"])
27
+ ],
28
+ dependencies: [
29
+ .package(path: "../nitro-core")
30
+ ],
31
+ targets: [
32
+ .target(
33
+ name: "\(name)Cxx",
34
+ dependencies: nitroDependencies,
35
+ path: "\(generated)/shared/c++",
36
+ exclude: ["include"],
37
+ publicHeadersPath: "include",
38
+ cxxSettings: cxxSettings
39
+ ),
40
+ .target(
41
+ name: "\(name)AppleCxx",
42
+ dependencies: [.target(name: "\(name)Cxx")] + nitroDependencies,
43
+ path: "\(generated)/apple/\(name)AppleCxx",
44
+ cxxSettings: cxxSettings
45
+ ),
46
+ .target(
47
+ name: name,
48
+ dependencies: [.target(name: "\(name)AppleCxx")] + nitroDependencies,
49
+ path: ".",
50
+ exclude: excludes(in: ".", keeping: swiftSources),
51
+ sources: swiftSources,
52
+ cxxSettings: cxxSettings,
53
+ swiftSettings: [
54
+ // UIKit implementations run synchronously on the host's main thread.
55
+ .swiftLanguageMode(.v5),
56
+ .unsafeFlags(["-strict-concurrency=minimal"]),
57
+ .enableUpcomingFeature("InternalImportsByDefault"),
58
+ .enableUpcomingFeature("MemberImportVisibility"),
59
+ .interoperabilityMode(.Cxx),
60
+ .unsafeFlags(["-Xfrontend", "-clang-header-expose-decls=has-expose-attr-or-stdlib"]),
61
+ .unsafeFlags([
62
+ "-Xfrontend", "-clang-header-expose-module",
63
+ "-Xfrontend", "NitroModules=NitroModules-Swift.h",
64
+ ]),
65
+ ]
66
+ ),
67
+ .target(
68
+ name: "\(name)SwiftBridge",
69
+ dependencies: [.target(name: name), .target(name: "\(name)AppleCxx")] + nitroDependencies,
70
+ path: "\(generated)/apple",
71
+ exclude: excludes(in: "\(generated)/apple", keeping: bridgeSources) + ["c++/include"],
72
+ sources: bridgeSources,
73
+ publicHeadersPath: "c++/include",
74
+ cxxSettings: cxxSettings + [
75
+ .unsafeFlags(["-I${GENERATED_MODULEMAP_DIR}", "-fno-modules"])
76
+ ]
77
+ ),
78
+ ],
79
+ cxxLanguageStandard: .cxx20
80
+ )
81
+
82
+ func excludes(in path: String, keeping sources: [String]) -> [String] {
83
+ var parents: Set<String> = [""]
84
+ for source in sources {
85
+ var components = source.split(separator: "/").dropLast()
86
+ while !components.isEmpty {
87
+ parents.insert(components.joined(separator: "/"))
88
+ components = components.dropLast()
89
+ }
90
+ }
91
+ let root = URL(fileURLWithPath: Context.packageDirectory).appendingPathComponent(path)
92
+ return parents.sorted().flatMap { parent in
93
+ let entries =
94
+ (try? FileManager.default.contentsOfDirectory(
95
+ atPath: root.appendingPathComponent(parent).path)) ?? []
96
+ return entries.sorted().compactMap { entry in
97
+ let entryPath = parent.isEmpty ? entry : "\(parent)/\(entry)"
98
+ return entry.hasPrefix(".") || sources.contains(entryPath) || parents.contains(entryPath)
99
+ ? nil : entryPath
100
+ }
101
+ }
102
+ }
package/README.md CHANGED
@@ -1,3 +1,59 @@
1
- # Temporary Holding Version
1
+ # React Nitro Networking
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ## Fetch
4
+
5
+ Call `Networking.fetch()` directly on the imported class:
6
+
7
+ ```ts
8
+ import { Networking, Response } from 'react-nitro-networking'
9
+
10
+ const response = await Networking.fetch('https://example.com')
11
+ console.log(response instanceof Response) // true
12
+ console.log(await response.text())
13
+ ```
14
+
15
+ ## WebSockets
16
+
17
+ `new WebSocket(url)` starts connecting immediately. Each socket supports text and
18
+ `ArrayBuffer` messages. Listeners are additive and return a subscription with an idempotent `remove()`.
19
+
20
+ ```ts
21
+ import { WebSocket } from 'react-nitro-networking'
22
+
23
+ const socket = new WebSocket('wss://example.com/socket')
24
+ const messages = socket.addOnMessageReceivedListener((data) => {
25
+ console.log(data)
26
+ })
27
+ socket.addOnOpenedListener(() => console.log('Connected'))
28
+ socket.addOnErrorListener((error) => console.error(error))
29
+ socket.addOnClosedListener(() => console.log('Closed'))
30
+
31
+ try {
32
+ // send() waits for the handshake if the connection is still opening.
33
+ await socket.send('Hello')
34
+ } catch (error) {
35
+ console.error('Could not send:', error)
36
+ }
37
+
38
+ // When the connection is no longer needed:
39
+ messages.remove()
40
+ socket.close()
41
+ ```
42
+
43
+ Binary messages use the same methods. Await sends to preserve their order:
44
+
45
+ ```ts
46
+ await socket.send(new Uint8Array([0, 1, 255]).buffer)
47
+ await socket.send(new ArrayBuffer(0))
48
+ ```
49
+
50
+ Invalid URLs throw from the constructor. Handshake and transport failures notify error listeners,
51
+ then closed listeners. Late opened listeners are notified if the socket is still open; late error
52
+ and closed listeners receive the terminal outcome. `close()` is idempotent, releases listeners,
53
+ and reports local shutdown without waiting for a peer acknowledgement. Reconnecting requires a new socket.
54
+
55
+ Receiving starts immediately. Until the first message listener is registered, up to 64 messages
56
+ or 1 MiB are buffered. Exceeding either limit closes the connection with an error. After the first
57
+ subscription, messages without listeners are discarded. Closing also discards buffered messages.
58
+
59
+ The current implementation supports Apple platforms through `URLSessionWebSocketTask`.
@@ -0,0 +1,17 @@
1
+ internal import NitroModules
2
+ internal import Foundation
3
+
4
+ final class HybridNetworking: HybridNetworkingSpec {
5
+ static let session = URLSession(configuration: .default)
6
+
7
+ static func fetch(url: String) throws -> any Promise<any HybridResponseSpec> {
8
+ guard let url = URL(string: url) else {
9
+ throw RuntimeError("Invalid URL: \"\(url)\"")
10
+ }
11
+ return .async { [session] in
12
+ let delegate = NetworkingTaskDelegate()
13
+ let (bytes, response) = try await session.bytes(from: url, delegate: delegate)
14
+ return try HybridResponse(bytes: bytes, response: response, redirected: delegate.redirected)
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,87 @@
1
+ //
2
+ // HybridResponse.swift
3
+ // ReactNitroNetworking
4
+ //
5
+ // Created by Marc Rousavy on 18.09.26.
6
+ //
7
+
8
+ internal import NitroModules
9
+ internal import Foundation
10
+
11
+ final class HybridResponse: HybridResponseSpec {
12
+ private let bytes: URLSession.AsyncBytes
13
+ private let response: URLResponse
14
+
15
+ var headers: [String: String] {
16
+ guard let response = response as? HTTPURLResponse else { return [:] }
17
+ var headers: [String: String] = [:]
18
+ for (name, value) in response.allHeaderFields {
19
+ guard let name = name as? String else { continue }
20
+ headers[name.lowercased()] = String(describing: value)
21
+ }
22
+ return headers
23
+ }
24
+ var status: Double { Double((response as? HTTPURLResponse)?.statusCode ?? 0) }
25
+ var ok: Bool { (200..<300).contains(status) }
26
+ var statusText: String { "" }
27
+ var url: String { response.url?.absoluteString ?? "" }
28
+ let redirected: Bool
29
+ var type: ResponseType { .basic }
30
+ private(set) var bodyUsed = false
31
+
32
+ private lazy var materializedData = Task<Data, Error> { [bytes, response] in
33
+ var data = Data()
34
+
35
+ let expected = response.expectedContentLength
36
+ if expected > 0 {
37
+ // Cap initial reservation size at 8MB to not explode memory
38
+ let reservation = Int(min(expected, 8 * 1024 * 1024))
39
+ data.reserveCapacity(reservation)
40
+ }
41
+
42
+ for try await byte in bytes {
43
+ data.append(byte)
44
+ }
45
+ return data
46
+ }
47
+ private var data: Data {
48
+ get async throws {
49
+ bodyUsed = true
50
+ let data = try await materializedData.value
51
+ return data
52
+ }
53
+ }
54
+
55
+ init(bytes: URLSession.AsyncBytes, response: URLResponse, redirected: Bool) throws {
56
+ self.bytes = bytes
57
+ self.response = response
58
+ self.redirected = redirected
59
+ try super.init()
60
+ }
61
+
62
+ func arrayBuffer() -> any Promise<ArrayBuffer> {
63
+ return .async {
64
+ // TODO: Maybe we can figure out a way to avoid copying `Data` here and instead map the `ArrayBuffer` zero-copy?
65
+ let data = try await self.data
66
+ return try ArrayBuffer.copy(data: data)
67
+ }
68
+ }
69
+
70
+ func json() -> any Promise<AnyJSON> {
71
+ return .async {
72
+ let data = try await self.data
73
+ // Like text(), skip a leading UTF-8 BOM.
74
+ let content = data.starts(with: [0xEF, 0xBB, 0xBF]) ? data.dropFirst(3) : data
75
+ return AnyJSON(data: content)
76
+ }
77
+ }
78
+
79
+ func text() -> any Promise<String> {
80
+ return .async {
81
+ let data = try await self.data
82
+ // Fetch decodes UTF-8 with replacement and strips a leading UTF-8 BOM.
83
+ let content = data.starts(with: [0xEF, 0xBB, 0xBF]) ? data.dropFirst(3) : data
84
+ return String(decoding: content, as: UTF8.self)
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,188 @@
1
+ internal import Darwin
2
+ internal import Foundation
3
+ internal import NitroModules
4
+ internal import NitroModulesCxx
5
+
6
+ final class HybridWebSocket: HybridWebSocketSpec, @unchecked Sendable {
7
+ private let task: URLSessionWebSocketTask
8
+ private let delegate = WebSocketTaskDelegate()
9
+ private let queue = DispatchQueue(label: "com.margelo.nitro.websocket")
10
+ private let openedListeners: WebSocketListeners<Void>
11
+ private let messageListeners: WebSocketListeners<Variant_ArrayBuffer_String>
12
+ private let closedListeners: WebSocketListeners<Void>
13
+ private let errorListeners: WebSocketListeners<Error>
14
+
15
+ // Accessed only on queue.
16
+ private var isClosed = false
17
+ private var receiveTask: Task<Void, Never>?
18
+ private var hasMessageListener = false
19
+ private var pendingMessages: [URLSessionWebSocketTask.Message] = []
20
+ private var pendingByteCount = 0
21
+
22
+ convenience init(url: String) throws {
23
+ try self.init(session: HybridNetworking.session, url: try WebSocketURL.parse(url))
24
+ }
25
+
26
+ init(session: URLSession, url: URL) throws {
27
+ task = session.webSocketTask(with: url)
28
+ openedListeners = WebSocketListeners(queue: queue)
29
+ messageListeners = WebSocketListeners(queue: queue)
30
+ closedListeners = WebSocketListeners(queue: queue)
31
+ errorListeners = WebSocketListeners(queue: queue)
32
+ try super.init()
33
+ delegate.socket = self
34
+ task.delegate = delegate
35
+ task.resume()
36
+ queue.async { self.startReceiving() }
37
+ }
38
+
39
+ deinit {
40
+ receiveTask?.cancel()
41
+ task.cancel(with: .goingAway, reason: nil)
42
+ }
43
+
44
+ func send(data: Variant_ArrayBuffer_String) throws -> any Promise<Void> {
45
+ let message: URLSessionWebSocketTask.Message
46
+ switch data {
47
+ case .first(let buffer):
48
+ message = .data(buffer.toData(copyIfNeeded: true))
49
+ case .second(let text):
50
+ message = .string(text)
51
+ }
52
+ return .async { [task] in
53
+ try await task.send(message)
54
+ }
55
+ }
56
+
57
+ func close() {
58
+ queue.async { self.finish() }
59
+ // Cancel before returning so a subsequent send cannot race local shutdown.
60
+ // The queued finish runs before any delegate notification caused by cancellation.
61
+ task.cancel(with: .normalClosure, reason: nil)
62
+ }
63
+
64
+ func dispose() {
65
+ close()
66
+ }
67
+
68
+ func addOnOpenedListener(listener: any Callback<(), Void>) -> ListenerSubscription {
69
+ return openedListeners.add(listener)
70
+ }
71
+
72
+ func addOnMessageReceivedListener(
73
+ listener: any Callback<(Variant_ArrayBuffer_String), Void>
74
+ ) -> ListenerSubscription {
75
+ return messageListeners.add(
76
+ listener,
77
+ onAdded: { [weak self] in
78
+ guard let self, !self.hasMessageListener else { return }
79
+ self.hasMessageListener = true
80
+ for message in self.pendingMessages {
81
+ self.emitMessage(message)
82
+ }
83
+ self.pendingMessages.removeAll()
84
+ self.pendingByteCount = 0
85
+ })
86
+ }
87
+
88
+ func addOnClosedListener(listener: any Callback<(), Void>) -> ListenerSubscription {
89
+ return closedListeners.add(listener)
90
+ }
91
+
92
+ func addOnErrorListener(listener: any Callback<(Error), Void>) -> ListenerSubscription {
93
+ return errorListeners.add(listener)
94
+ }
95
+
96
+ func didOpen() {
97
+ queue.async {
98
+ guard !self.isClosed else { return }
99
+ self.openedListeners.finish(replaying: ())
100
+ }
101
+ }
102
+
103
+ func didComplete(error: Error?) {
104
+ queue.async {
105
+ // Once receiving, let the receive loop deliver preceding messages before closing.
106
+ if self.receiveTask == nil {
107
+ self.finish(error: error)
108
+ }
109
+ }
110
+ }
111
+
112
+ private func startReceiving() {
113
+ guard !isClosed, receiveTask == nil else { return }
114
+ receiveTask = Task { [weak self, task] in
115
+ do {
116
+ while !Task.isCancelled {
117
+ let message = try await task.receive()
118
+ self?.didReceive(message)
119
+ }
120
+ } catch {
121
+ // A peer close can also end receive() with an error. Its close code distinguishes
122
+ // a received close frame from a transport failure.
123
+ let failure = task.closeCode == .invalid || task.closeCode == .abnormalClosure ? error : nil
124
+ self?.queue.async { [weak self] in self?.finish(error: failure) }
125
+ }
126
+ }
127
+ }
128
+
129
+ private func didReceive(_ message: URLSessionWebSocketTask.Message) {
130
+ queue.async {
131
+ guard !self.isClosed else { return }
132
+ if !self.hasMessageListener {
133
+ let byteCount: Int
134
+ switch message {
135
+ case .string(let text): byteCount = text.utf8.count
136
+ case .data(let data): byteCount = data.count
137
+ @unknown default:
138
+ self.finish(error: RuntimeError("Unsupported WebSocket message type"))
139
+ return
140
+ }
141
+ guard self.pendingMessages.count < 64, self.pendingByteCount + byteCount <= 1_048_576 else {
142
+ self.finish(
143
+ error: RuntimeError(
144
+ "WebSocket receive buffer is full. Register a message listener before receiving more messages."
145
+ ))
146
+ return
147
+ }
148
+ self.pendingMessages.append(message)
149
+ self.pendingByteCount += byteCount
150
+ } else {
151
+ self.emitMessage(message)
152
+ }
153
+ }
154
+ }
155
+
156
+ private func emitMessage(_ message: URLSessionWebSocketTask.Message) {
157
+ guard !isClosed else { return }
158
+ do {
159
+ let value: Variant_ArrayBuffer_String
160
+ switch message {
161
+ case .string(let text):
162
+ value = .second(text)
163
+ case .data(let data):
164
+ let buffer = data.isEmpty ? ArrayBuffer.allocate(0) : try ArrayBuffer.copy(data: data)
165
+ value = .first(buffer)
166
+ @unknown default:
167
+ throw RuntimeError("Unsupported WebSocket message type")
168
+ }
169
+ messageListeners.emit(value)
170
+ } catch {
171
+ finish(error: error)
172
+ }
173
+ }
174
+
175
+ private func finish(error: Error? = nil) {
176
+ guard !isClosed else { return }
177
+ isClosed = true
178
+ receiveTask?.cancel()
179
+ receiveTask = nil
180
+ pendingMessages.removeAll()
181
+ pendingByteCount = 0
182
+ task.cancel(with: .normalClosure, reason: nil)
183
+ openedListeners.finish()
184
+ messageListeners.finish()
185
+ errorListeners.finish(replaying: error)
186
+ closedListeners.finish(replaying: ())
187
+ }
188
+ }
@@ -0,0 +1,16 @@
1
+ internal import Foundation
2
+
3
+ final class NetworkingTaskDelegate: NSObject, URLSessionTaskDelegate {
4
+ private(set) var redirected = false
5
+
6
+ func urlSession(
7
+ _ session: URLSession,
8
+ task: URLSessionTask,
9
+ willPerformHTTPRedirection response: HTTPURLResponse,
10
+ newRequest request: URLRequest,
11
+ completionHandler: @escaping (URLRequest?) -> Void
12
+ ) {
13
+ redirected = true
14
+ completionHandler(request)
15
+ }
16
+ }
@@ -0,0 +1,49 @@
1
+ internal import Foundation
2
+ internal import NitroModules
3
+
4
+ /// All registry state and emissions belong to the socket's serial queue.
5
+ final class WebSocketListeners<Value> {
6
+ private let queue: DispatchQueue
7
+ private var listeners: [UUID: any Callback<(Value), Void>] = [:]
8
+ private var replay: Value?
9
+ private var isFinished = false
10
+
11
+ init(queue: DispatchQueue) {
12
+ self.queue = queue
13
+ }
14
+
15
+ func add(
16
+ _ listener: any Callback<(Value), Void>,
17
+ onAdded: @escaping () -> Void = {}
18
+ ) -> ListenerSubscription {
19
+ let id = UUID()
20
+ queue.async {
21
+ if let value = self.replay {
22
+ listener.callAndForget(value)
23
+ }
24
+ if !self.isFinished {
25
+ self.listeners[id] = listener
26
+ onAdded()
27
+ }
28
+ }
29
+ return ListenerSubscription(remove: .native { [weak self] in
30
+ guard let self else { return }
31
+ self.queue.async { self.listeners.removeValue(forKey: id) }
32
+ })
33
+ }
34
+
35
+ func emit(_ value: Value) {
36
+ for listener in listeners.values {
37
+ listener.callAndForget(value)
38
+ }
39
+ }
40
+
41
+ func finish(replaying value: Value? = nil) {
42
+ if let value {
43
+ emit(value)
44
+ }
45
+ replay = value
46
+ isFinished = true
47
+ listeners.removeAll()
48
+ }
49
+ }
@@ -0,0 +1,26 @@
1
+ internal import Foundation
2
+
3
+ final class WebSocketTaskDelegate: NSObject, URLSessionWebSocketDelegate {
4
+ weak var socket: HybridWebSocket?
5
+
6
+ func urlSession(
7
+ _ session: URLSession,
8
+ webSocketTask: URLSessionWebSocketTask,
9
+ didOpenWithProtocol protocol: String?
10
+ ) {
11
+ socket?.didOpen()
12
+ }
13
+
14
+ func urlSession(
15
+ _ session: URLSession,
16
+ webSocketTask: URLSessionWebSocketTask,
17
+ didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
18
+ reason: Data?
19
+ ) {
20
+ socket?.didComplete(error: nil)
21
+ }
22
+
23
+ func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
24
+ socket?.didComplete(error: error)
25
+ }
26
+ }
@@ -0,0 +1,18 @@
1
+ internal import Foundation
2
+ internal import NitroModules
3
+
4
+ enum WebSocketURL {
5
+ static func parse(_ value: String) throws -> URL {
6
+ guard let url = URL(string: value),
7
+ let scheme = url.scheme?.lowercased(),
8
+ scheme == "ws" || scheme == "wss",
9
+ let host = url.host, !host.isEmpty,
10
+ url.fragment == nil
11
+ else {
12
+ throw RuntimeError(
13
+ "Invalid WebSocket URL: \"\(value)\". Expected an absolute ws:// or wss:// URL without a fragment."
14
+ )
15
+ }
16
+ return url
17
+ }
18
+ }
@@ -0,0 +1 @@
1
+ ** linguist-generated=true