├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .paket ├── Paket.Restore.targets ├── paket.exe ├── paket.exe.config └── paket.targets ├── .travis.yml ├── Hopac.Websockets.sln ├── LICENSE.md ├── README.md ├── RELEASE_NOTES.md ├── appveyor.yml ├── build.cmd ├── build.fsx ├── build.sh ├── example ├── .gitignore ├── .paket │ ├── Paket.Restore.targets │ └── paket.exe ├── build.cmd ├── build.fsx ├── build.sh ├── global.json ├── package.json ├── paket.dependencies ├── paket.lock ├── src │ ├── Client │ │ ├── Client.fs │ │ ├── Client.fsproj │ │ ├── Images │ │ │ └── safe_favicon.png │ │ ├── index.html │ │ ├── paket.references │ │ └── webpack.config.js │ ├── Server │ │ ├── Server.fs │ │ ├── Server.fsproj │ │ └── paket.references │ └── Shared │ │ └── Shared.fs └── yarn.lock ├── paket.dependencies ├── paket.lock ├── src ├── Hopac.Websockets.AspNetCore │ ├── AssemblyInfo.fs │ ├── Hopac.Websockets.AspNetCore.fs │ ├── Hopac.Websockets.AspNetCore.fsproj │ └── paket.references └── Hopac.Websockets │ ├── AssemblyInfo.fs │ ├── Hopac.Websockets.fs │ ├── Hopac.Websockets.fsproj │ └── paket.references └── tests └── Hopac.Websockets.Tests ├── App.config ├── AssemblyInfo.fs ├── Hopac.Websockets.Tests.fsproj ├── Main.fs ├── Tests.fs └── paket.references /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: 2 | http://EditorConfig.org 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # Default settings: 8 | # A newline ending every file 9 | # Use 4 spaces as indentation 10 | [*] 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{fs,fsi,fsx,config}] 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | 19 | [paket.*] 20 | trim_trailing_whitespace = true 21 | indent_size = 2 22 | 23 | [*.paket.references] 24 | trim_trailing_whitespace = true 25 | indent_size = 2 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please insert a description of your problem or question. 4 | 5 | ## Error messages, screenshots 6 | 7 | Please add any error logs or screenshots if available. 8 | 9 | ## Failing test, failing github repo, or reproduction steps 10 | 11 | Please add either a failing test, a github repo of the problem or detailed reproduction steps. 12 | 13 | ## Expected Behavior 14 | 15 | Please define what you would expect the behavior to be like. 16 | 17 | ## Known workarounds 18 | 19 | Please provide a description of any known workarounds. 20 | 21 | ## Other information 22 | 23 | * Operating System: 24 | - [ ] windows [insert version here] 25 | - [ ] macOs [insert version] 26 | - [ ] linux [insert flavor/version here] 27 | * Platform 28 | - [ ] dotnet core 29 | - [ ] dotnet full 30 | - [ ] mono 31 | * Branch or release version: 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Proposed Changes 2 | 3 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 4 | 5 | ## Types of changes 6 | 7 | What types of changes does your code introduce to Appium? 8 | _Put an `x` in the boxes that apply_ 9 | 10 | - [ ] Bugfix (non-breaking change which fixes an issue) 11 | - [ ] New feature (non-breaking change which adds functionality) 12 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 13 | 14 | 15 | ## Checklist 16 | 17 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 18 | 19 | - [ ] Build and tests pass locally 20 | - [ ] I have added tests that prove my fix is effective or that my feature works (if appropriate) 21 | - [ ] I have added necessary documentation (if appropriate) 22 | 23 | ## Further comments 24 | 25 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | packages/ 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.pfx 193 | *.publishsettings 194 | node_modules/ 195 | orleans.codegen.cs 196 | 197 | # Since there are multiple workflows, uncomment next line to ignore bower_components 198 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 199 | #bower_components/ 200 | 201 | # RIA/Silverlight projects 202 | Generated_Code/ 203 | 204 | # Backup & report files from converting an old project file 205 | # to a newer Visual Studio version. Backup files are not needed, 206 | # because we have git ;-) 207 | _UpgradeReport_Files/ 208 | Backup*/ 209 | UpgradeLog*.XML 210 | UpgradeLog*.htm 211 | 212 | # SQL Server files 213 | *.mdf 214 | *.ldf 215 | 216 | # Business Intelligence projects 217 | *.rdl.data 218 | *.bim.layout 219 | *.bim_*.settings 220 | 221 | # Microsoft Fakes 222 | FakesAssemblies/ 223 | 224 | # GhostDoc plugin setting file 225 | *.GhostDoc.xml 226 | 227 | # Node.js Tools for Visual Studio 228 | .ntvs_analysis.dat 229 | 230 | # Visual Studio 6 build log 231 | *.plg 232 | 233 | # Visual Studio 6 workspace options file 234 | *.opt 235 | 236 | # Visual Studio LightSwitch build output 237 | **/*.HTMLClient/GeneratedArtifacts 238 | **/*.DesktopClient/GeneratedArtifacts 239 | **/*.DesktopClient/ModelManifest.xml 240 | **/*.Server/GeneratedArtifacts 241 | **/*.Server/ModelManifest.xml 242 | _Pvt_Extensions 243 | 244 | # Paket dependency manager 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | 254 | TestResults.xml 255 | 256 | dist/ 257 | -------------------------------------------------------------------------------- /.paket/Paket.Restore.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 8 | 9 | true 10 | $(MSBuildThisFileDirectory) 11 | $(MSBuildThisFileDirectory)..\ 12 | $(PaketRootPath)paket-files\paket.restore.cached 13 | $(PaketRootPath)paket.lock 14 | /Library/Frameworks/Mono.framework/Commands/mono 15 | mono 16 | 17 | $(PaketRootPath)paket.exe 18 | $(PaketToolsPath)paket.exe 19 | "$(PaketExePath)" 20 | $(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 21 | 22 | 23 | <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)")) 24 | dotnet "$(PaketExePath)" 25 | 26 | 27 | "$(PaketExePath)" 28 | 29 | $(PaketRootPath)paket.bootstrapper.exe 30 | $(PaketToolsPath)paket.bootstrapper.exe 31 | "$(PaketBootStrapperExePath)" 32 | $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)" 33 | 34 | 35 | 36 | 37 | true 38 | true 39 | 40 | 41 | 42 | 43 | 44 | 45 | true 46 | $(NoWarn);NU1603 47 | 48 | 49 | 50 | 51 | /usr/bin/shasum $(PaketRestoreCacheFile) | /usr/bin/awk '{ print $1 }' 52 | /usr/bin/shasum $(PaketLockFilePath) | /usr/bin/awk '{ print $1 }' 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) 66 | $([System.IO.File]::ReadAllText('$(PaketLockFilePath)')) 67 | true 68 | false 69 | true 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | $(MSBuildProjectDirectory)\obj\$(MSBuildProjectFile).paket.references.cached 79 | 80 | $(MSBuildProjectFullPath).paket.references 81 | 82 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 83 | 84 | $(MSBuildProjectDirectory)\paket.references 85 | $(MSBuildProjectDirectory)\obj\$(MSBuildProjectFile).$(TargetFramework).paket.resolved 86 | true 87 | references-file-or-cache-not-found 88 | 89 | 90 | 91 | 92 | $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)')) 93 | $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)')) 94 | references-file 95 | false 96 | 97 | 98 | 99 | 100 | false 101 | 102 | 103 | 104 | 105 | true 106 | target-framework '$(TargetFramework)' 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) 124 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) 125 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4]) 126 | 127 | 128 | %(PaketReferencesFileLinesInfo.PackageVersion) 129 | All 130 | 131 | 132 | 133 | 134 | $(MSBuildProjectDirectory)/obj/$(MSBuildProjectFile).paket.clitools 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0]) 144 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1]) 145 | 146 | 147 | %(PaketCliToolFileLinesInfo.PackageVersion) 148 | 149 | 150 | 151 | 155 | 156 | 157 | 158 | 159 | 160 | false 161 | 162 | 163 | 164 | 165 | 166 | <_NuspecFilesNewLocation Include="$(BaseIntermediateOutputPath)$(Configuration)\*.nuspec"/> 167 | 168 | 169 | 170 | $(MSBuildProjectDirectory)/$(MSBuildProjectFile) 171 | true 172 | false 173 | true 174 | $(BaseIntermediateOutputPath)$(Configuration) 175 | $(BaseIntermediateOutputPath) 176 | 177 | 178 | 179 | <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.nuspec"/> 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 232 | 233 | 274 | 275 | 276 | 277 | -------------------------------------------------------------------------------- /.paket/paket.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAngryByrd/Hopac.Websockets/35fffb1d9f381b6982a2e34bfcdfb826d0ae85b4/.paket/paket.exe -------------------------------------------------------------------------------- /.paket/paket.exe.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.paket/paket.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | true 7 | $(MSBuildThisFileDirectory) 8 | $(MSBuildThisFileDirectory)..\ 9 | /Library/Frameworks/Mono.framework/Commands/mono 10 | mono 11 | 12 | 13 | 14 | 15 | $(PaketRootPath)paket.exe 16 | $(PaketToolsPath)paket.exe 17 | "$(PaketExePath)" 18 | $(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 19 | 20 | 21 | 22 | 23 | 24 | $(MSBuildProjectFullPath).paket.references 25 | 26 | 27 | 28 | 29 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 30 | 31 | 32 | 33 | 34 | $(MSBuildProjectDirectory)\paket.references 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 47 | $(MSBuildProjectDirectory)\paket.references 48 | $(MSBuildStartupDirectory)\paket.references 49 | $(MSBuildProjectFullPath).paket.references 50 | $(PaketCommand) restore --references-files "$(PaketReferences)" 51 | 52 | RestorePackages; $(BuildDependsOn); 53 | 54 | 55 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | sudo: required 3 | dist: trusty 4 | 5 | dotnet: 2.1.4 6 | mono: 7 | - 5.10.0 8 | - latest # => "stable release" 9 | - alpha 10 | - beta 11 | - weekly # => "latest commits" 12 | os: 13 | - linux 14 | 15 | addons: 16 | apt: 17 | packages: 18 | - dotnet-sharedframework-microsoft.netcore.app-1.1.2 19 | 20 | before_script: 21 | - if [ "$TRAVIS_OS_NAME" == "linux" ]; 22 | then export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:~/.nuget/packages/libuv/1.10.0/runtimes/linux-x64/native; 23 | else ln -s ~/.nuget/packages/libuv/1.10.0/runtimes/osx/native/libuv.dylib /usr/local/lib/libuv.dylib; 24 | fi 25 | 26 | script: 27 | - ./build.sh 28 | 29 | matrix: 30 | fast_finish: true 31 | allow_failures: 32 | - mono: latest 33 | - mono: alpha 34 | - mono: beta 35 | - mono: weekly 36 | -------------------------------------------------------------------------------- /Hopac.Websockets.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".paket", ".paket", "{AB46463E-5B14-4B9D-A575-5A6EDE377EE5}" 7 | ProjectSection(SolutionItems) = preProject 8 | paket.dependencies = paket.dependencies 9 | EndProjectSection 10 | EndProject 11 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C397A34C-84F1-49E7-AEBC-2F9F2B196216}" 12 | EndProject 13 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Hopac.Websockets", "src\Hopac.Websockets\Hopac.Websockets.fsproj", "{5D30E174-2538-47AC-8443-318C8C5DC2C9}" 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{ACBEE43C-7A88-4FB1-9B06-DB064D22B29F}" 16 | EndProject 17 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Hopac.Websockets.Tests", "tests\Hopac.Websockets.Tests\Hopac.Websockets.Tests.fsproj", "{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}" 18 | EndProject 19 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Hopac.Websockets.AspNetCore", "src\Hopac.Websockets.AspNetCore\Hopac.Websockets.AspNetCore.fsproj", "{96FC30AA-D5BA-4391-9D43-C2D415DE4166}" 20 | EndProject 21 | Global 22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 23 | Debug|Any CPU = Debug|Any CPU 24 | Debug|x64 = Debug|x64 25 | Debug|x86 = Debug|x86 26 | Release|Any CPU = Release|Any CPU 27 | Release|x64 = Release|x64 28 | Release|x86 = Release|x86 29 | EndGlobalSection 30 | GlobalSection(SolutionProperties) = preSolution 31 | HideSolutionNode = FALSE 32 | EndGlobalSection 33 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 34 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x64.ActiveCfg = Debug|x64 37 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x64.Build.0 = Debug|x64 38 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x86.ActiveCfg = Debug|x86 39 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x86.Build.0 = Debug|x86 40 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x64.ActiveCfg = Release|x64 43 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x64.Build.0 = Release|x64 44 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x86.ActiveCfg = Release|x86 45 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x86.Build.0 = Release|x86 46 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x64.ActiveCfg = Debug|x64 49 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x64.Build.0 = Debug|x64 50 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x86.ActiveCfg = Debug|x86 51 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x86.Build.0 = Debug|x86 52 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x64.ActiveCfg = Release|x64 55 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x64.Build.0 = Release|x64 56 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x86.ActiveCfg = Release|x86 57 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x86.Build.0 = Release|x86 58 | {96FC30AA-D5BA-4391-9D43-C2D415DE4166}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {96FC30AA-D5BA-4391-9D43-C2D415DE4166}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {96FC30AA-D5BA-4391-9D43-C2D415DE4166}.Debug|x64.ActiveCfg = Debug|x64 61 | {96FC30AA-D5BA-4391-9D43-C2D415DE4166}.Debug|x64.Build.0 = Debug|x64 62 | {96FC30AA-D5BA-4391-9D43-C2D415DE4166}.Debug|x86.ActiveCfg = Debug|x86 63 | {96FC30AA-D5BA-4391-9D43-C2D415DE4166}.Debug|x86.Build.0 = Debug|x86 64 | {96FC30AA-D5BA-4391-9D43-C2D415DE4166}.Release|Any CPU.ActiveCfg = Release|Any CPU 65 | {96FC30AA-D5BA-4391-9D43-C2D415DE4166}.Release|Any CPU.Build.0 = Release|Any CPU 66 | {96FC30AA-D5BA-4391-9D43-C2D415DE4166}.Release|x64.ActiveCfg = Release|x64 67 | {96FC30AA-D5BA-4391-9D43-C2D415DE4166}.Release|x64.Build.0 = Release|x64 68 | {96FC30AA-D5BA-4391-9D43-C2D415DE4166}.Release|x86.ActiveCfg = Release|x86 69 | {96FC30AA-D5BA-4391-9D43-C2D415DE4166}.Release|x86.Build.0 = Release|x86 70 | EndGlobalSection 71 | GlobalSection(NestedProjects) = preSolution 72 | {5D30E174-2538-47AC-8443-318C8C5DC2C9} = {C397A34C-84F1-49E7-AEBC-2F9F2B196216} 73 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA} = {ACBEE43C-7A88-4FB1-9B06-DB064D22B29F} 74 | {96FC30AA-D5BA-4391-9D43-C2D415DE4166} = {C397A34C-84F1-49E7-AEBC-2F9F2B196216} 75 | EndGlobalSection 76 | EndGlobal 77 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 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 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hopac.Websockets 2 | 3 | A threadsafe [Hopac](https://github.com/Hopac/Hopac) wrapper around [dotnet websockets](https://docs.microsoft.com/en-us/dotnet/api/system.net.websockets.websocket?view=netcore-2.0). 4 | 5 | 6 | ### Why? 7 | 8 | Dotnet websockets only allow for one receive and one send at a time. If multiple threads try to write to a websocket, it will throw a `System.InvalidOperationException` with the message `There is already one outstanding 'SendAsync' call for this WebSocket instance. ReceiveAsync and SendAsync can be called simultaneously, but at most one outstanding operation for each of them is allowed at the same time.`. This wraps a websocket in a hopac server-client model that allows for multiple threads to write or read at the same time. See https://docs.microsoft.com/en-us/dotnet/api/system.net.websockets.websocket.sendasync?view=netcore-2.0#Remarks 9 | 10 | --- 11 | 12 | ## Builds 13 | 14 | MacOS/Linux | Windows 15 | --- | --- 16 | [![Travis Badge](https://travis-ci.org/TheAngryByrd/Hopac.Websockets.svg?branch=master)](https://travis-ci.org/TheAngryByrd/Hopac.Websockets) | [![Build status](https://ci.appveyor.com/api/projects/status/github/TheAngryByrd/Hopac.Websockets?svg=true)](https://ci.appveyor.com/project/TheAngryByrd/Hopac-Websockets) 17 | [![Build History](https://buildstats.info/travisci/chart/TheAngryByrd/Hopac.Websockets)](https://travis-ci.org/TheAngryByrd/Hopac.Websockets/builds) | [![Build History](https://buildstats.info/appveyor/chart/TheAngryByrd/Hopac-Websockets)](https://ci.appveyor.com/project/TheAngryByrd/Hopac-Websockets) 18 | 19 | 20 | ## Nuget 21 | 22 | Package | Stable | Prerelease 23 | --- | --- | --- 24 | Hopac.Websockets | [![NuGet Badge](https://buildstats.info/nuget/Hopac.Websockets)](https://www.nuget.org/packages/Hopac.Websockets/) | [![NuGet Badge](https://buildstats.info/nuget/Hopac.Websockets?includePreReleases=true)](https://www.nuget.org/packages/Hopac.Websockets/) 25 | Hopac.Websockets.AspNetCore | [![NuGet Badge](https://buildstats.info/nuget/Hopac.Websockets.AspNetCore)](https://www.nuget.org/packages/Hopac.Websockets.AspNetCore/) | [![NuGet Badge](https://buildstats.info/nuget/Hopac.Websockets.AspNetCore?includePreReleases=true)](https://www.nuget.org/packages/Hopac.Websockets.AspNetCore/) 26 | 27 | --- 28 | 29 | ### Building 30 | 31 | 32 | Make sure the following **requirements** are installed in your system: 33 | 34 | * [dotnet SDK](https://www.microsoft.com/net/download/core) 2.0 or higher 35 | * [Mono](http://www.mono-project.com/) if you're on Linux or macOS. 36 | 37 | ``` 38 | > build.cmd // on windows 39 | $ ./build.sh // on unix 40 | ``` 41 | 42 | 43 | ### Watch Tests 44 | 45 | The `WatchTests` target will use [dotnet-watch](https://github.com/aspnet/Docs/blob/master/aspnetcore/tutorials/dotnet-watch.md) to watch for changes in your lib or tests and re-run your tests on all `TargetFrameworks` 46 | 47 | ``` 48 | ./build.sh WatchTests 49 | ``` 50 | 51 | ### Releasing 52 | 53 | * [Add your nuget API key to paket](https://fsprojects.github.io/Paket/paket-config.html#Adding-a-NuGet-API-key) 54 | 55 | ``` 56 | paket config add-token "https://www.nuget.org" 4003d786-cc37-4004-bfdf-c4f3e8ef9b3a 57 | ``` 58 | 59 | 60 | * Then update the `RELEASE_NOTES.md` with a new version, date, and release notes [ReleaseNotesHelper](https://fsharp.github.io/FAKE/apidocs/fake-releasenoteshelper.html) 61 | 62 | ``` 63 | #### 0.2.0 - 2017-04-20 64 | * FEATURE: Does cool stuff! 65 | * BUGFIX: Fixes that silly oversight 66 | ``` 67 | 68 | * You can then use the `Release` target. This will: 69 | * make a commit bumping the version: `Bump version to 0.2.0` and add the release notes to the commit 70 | * publish the package to nuget 71 | * push a git tag 72 | 73 | ``` 74 | ./build.sh Release 75 | ``` 76 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | ### 0.4.0 - 2018-05-16 2 | * FEATURE: Made ThreadSafeWebSocket implement IDisposable and expose other members (https://github.com/TheAngryByrd/Hopac.Websockets/pull/4) 3 | 4 | ### 0.3.0 - 2018-04-03 5 | * FEATURE: Created Hopac.Websockets.AspNetCore package with AcceptThreadSafeWebsocket extension method added to the WebSocketManager for Microsoft.AspNetCore.Http.Abstractions (https://github.com/TheAngryByrd/Hopac.Websockets/pull/3) 6 | 7 | ### 0.2.0 - 2018-04-02 8 | * FEATURE: Expose underlying websocket (https://github.com/TheAngryByrd/Hopac.Websockets/pull/2) 9 | 10 | #### 0.1.1 - 2018-03-14 11 | * BUGFIX: Fix net461 tests and add `Alt.tryIn` to websocket calls in ThreadsafeWebsocket server (https://github.com/TheAngryByrd/Hopac.Websockets/pull/1) 12 | 13 | #### 0.1.0 - 2018-03-14 14 | * Initial release 15 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2017 2 | init: 3 | - git config --global core.autocrlf input 4 | build_script: 5 | - cmd: build.cmd 6 | test: off 7 | version: 0.0.1.{build} 8 | artifacts: 9 | - path: bin 10 | name: bin 11 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | cls 3 | 4 | .paket\paket.exe restore 5 | if errorlevel 1 ( 6 | exit /b %errorlevel% 7 | ) 8 | 9 | IF NOT EXIST build.fsx ( 10 | .paket\paket.exe update 11 | packages\build\FAKE\tools\FAKE.exe init.fsx 12 | ) 13 | packages\build\FAKE\tools\FAKE.exe build.fsx %* 14 | -------------------------------------------------------------------------------- /build.fsx: -------------------------------------------------------------------------------- 1 | #r @"packages/build/FAKE/tools/FakeLib.dll" 2 | open Fake 3 | open Fake.Git 4 | open Fake.AssemblyInfoFile 5 | open Fake.ReleaseNotesHelper 6 | open Fake.UserInputHelper 7 | open System 8 | 9 | let release = LoadReleaseNotes "RELEASE_NOTES.md" 10 | let productName = "Hopac.Websockets" 11 | let sln = "Hopac.Websockets.sln" 12 | let srcGlob = "src/**/*.fsproj" 13 | let testsGlob = "tests/**/*.fsproj" 14 | 15 | Target "Clean" (fun _ -> 16 | ["bin"; "temp" ;"dist"] 17 | |> CleanDirs 18 | 19 | !! srcGlob 20 | ++ testsGlob 21 | |> Seq.collect(fun p -> 22 | ["bin";"obj"] 23 | |> Seq.map(fun sp -> 24 | IO.Path.GetDirectoryName p @@ sp) 25 | ) 26 | |> CleanDirs 27 | 28 | ) 29 | 30 | Target "DotnetRestore" (fun _ -> 31 | DotNetCli.Restore (fun c -> 32 | { c with 33 | Project = sln 34 | //This makes sure that Proj2 references the correct version of Proj1 35 | AdditionalArgs = [sprintf "/p:PackageVersion=%s" release.NugetVersion] 36 | })) 37 | 38 | Target "DotnetBuild" (fun _ -> 39 | DotNetCli.Build (fun c -> 40 | { c with 41 | Project = sln 42 | //This makes sure that Proj2 references the correct version of Proj1 43 | AdditionalArgs = 44 | [ 45 | sprintf "/p:PackageVersion=%s" release.NugetVersion 46 | "--no-restore" 47 | ] 48 | })) 49 | 50 | let invoke f = f () 51 | let invokeAsync f = async { f () } 52 | 53 | type TargetFramework = 54 | | Full of string 55 | | Core of string 56 | 57 | let (|StartsWith|_|) prefix (s: string) = 58 | if s.StartsWith prefix then Some () else None 59 | 60 | let getTargetFramework tf = 61 | match tf with 62 | | StartsWith "net4" -> Full tf 63 | | StartsWith "netcoreapp" -> Core tf 64 | | _ -> failwithf "Unknown TargetFramework %s" tf 65 | 66 | let getTargetFrameworksFromProjectFile (projFile : string)= 67 | let doc = Xml.XmlDocument() 68 | doc.Load(projFile) 69 | doc.GetElementsByTagName("TargetFrameworks").[0].InnerText.Split(';') 70 | |> Seq.map getTargetFramework 71 | |> Seq.toList 72 | 73 | let selectRunnerForFramework tf = 74 | let runMono = sprintf "mono -f %s -c Release --loggerlevel Warn" 75 | let runCore = sprintf "run -f %s -c Release" 76 | match tf with 77 | | Full t when isMono-> runMono t 78 | | Full t -> runCore t 79 | | Core t -> runCore t 80 | 81 | let addLogNameParamToArgs tf args = 82 | let frameworkName = 83 | match tf with 84 | | Full t -> t 85 | | Core t -> t 86 | sprintf "%s -- --log-name Expecto.%s" args frameworkName 87 | 88 | let runTests modifyArgs = 89 | !! testsGlob 90 | |> Seq.map(fun proj -> proj, getTargetFrameworksFromProjectFile proj) 91 | |> Seq.collect(fun (proj, targetFrameworks) -> 92 | targetFrameworks 93 | |> Seq.map (fun tf -> fun () -> 94 | DotNetCli.RunCommand (fun c -> 95 | { c with 96 | WorkingDir = IO.Path.GetDirectoryName proj 97 | }) (tf |> selectRunnerForFramework |> modifyArgs |> addLogNameParamToArgs tf)) 98 | ) 99 | 100 | 101 | Target "DotnetTest" (fun _ -> 102 | runTests (sprintf "%s --no-build") 103 | |> Seq.iter invoke 104 | 105 | ) 106 | let execProcAndReturnMessages filename args = 107 | let args' = args |> String.concat " " 108 | ProcessHelper.ExecProcessAndReturnMessages 109 | (fun psi -> 110 | psi.FileName <- filename 111 | psi.Arguments <-args' 112 | ) (TimeSpan.FromMinutes(1.)) 113 | 114 | let pkill args = 115 | execProcAndReturnMessages "pkill" args 116 | 117 | let killParentsAndChildren processId= 118 | pkill [sprintf "-P %d" processId] 119 | 120 | 121 | Target "WatchTests" (fun _ -> 122 | runTests (sprintf "watch %s --no-restore") 123 | |> Seq.iter (invokeAsync >> Async.Catch >> Async.Ignore >> Async.Start) 124 | 125 | printfn "Press Ctrl+C (or Ctrl+Break) to stop..." 126 | let cancelEvent = Console.CancelKeyPress |> Async.AwaitEvent |> Async.RunSynchronously 127 | cancelEvent.Cancel <- true 128 | 129 | if isWindows |> not then 130 | startedProcesses 131 | |> Seq.iter(fst >> killParentsAndChildren >> ignore ) 132 | else 133 | //Hope windows handles this right? 134 | () 135 | ) 136 | 137 | Target "AssemblyInfo" (fun _ -> 138 | let releaseChannel = 139 | match release.SemVer.PreRelease with 140 | | Some pr -> pr.Name 141 | | _ -> "release" 142 | let getAssemblyInfoAttributes projectName = 143 | [ Attribute.Title (projectName) 144 | Attribute.Product productName 145 | // Attribute.Description summary 146 | Attribute.Version release.AssemblyVersion 147 | Attribute.Metadata("ReleaseDate", release.Date.Value.ToString("o")) 148 | Attribute.FileVersion release.AssemblyVersion 149 | Attribute.InformationalVersion release.AssemblyVersion 150 | Attribute.Metadata("ReleaseChannel", releaseChannel) 151 | Attribute.Metadata("GitHash", Information.getCurrentSHA1(null)) 152 | ] 153 | 154 | let getProjectDetails projectPath = 155 | let projectName = System.IO.Path.GetFileNameWithoutExtension(projectPath) 156 | ( projectPath, 157 | projectName, 158 | System.IO.Path.GetDirectoryName(projectPath), 159 | (getAssemblyInfoAttributes projectName) 160 | ) 161 | 162 | !! "src/**/*.??proj" 163 | ++ "tests/**/*.??proj" 164 | |> Seq.map getProjectDetails 165 | |> Seq.iter (fun (projFileName, projectName, folderName, attributes) -> 166 | match projFileName with 167 | | Fsproj -> CreateFSharpAssemblyInfo (folderName @@ "AssemblyInfo.fs") attributes 168 | | Csproj -> CreateCSharpAssemblyInfo ((folderName @@ "Properties") @@ "AssemblyInfo.cs") attributes 169 | | Vbproj -> CreateVisualBasicAssemblyInfo ((folderName @@ "My Project") @@ "AssemblyInfo.vb") attributes 170 | | _ -> () 171 | ) 172 | ) 173 | 174 | Target "DotnetPack" (fun _ -> 175 | !! srcGlob 176 | |> Seq.iter (fun proj -> 177 | DotNetCli.Pack (fun c -> 178 | { c with 179 | Project = proj 180 | Configuration = "Release" 181 | OutputPath = IO.Directory.GetCurrentDirectory() @@ "dist" 182 | AdditionalArgs = 183 | [ 184 | sprintf "/p:PackageVersion=%s" release.NugetVersion 185 | sprintf "/p:PackageReleaseNotes=\"%s\"" (String.Join("\n",release.Notes)) 186 | "/p:SourceLinkCreate=true" 187 | ] 188 | }) 189 | ) 190 | ) 191 | 192 | Target "Publish" (fun _ -> 193 | Paket.Push(fun c -> 194 | { c with 195 | PublishUrl = "https://www.nuget.org" 196 | WorkingDir = "dist" 197 | } 198 | ) 199 | ) 200 | 201 | 202 | 203 | Target "Release" (fun _ -> 204 | if Git.Information.getBranchName "" <> "master" then failwith "Not on master" 205 | 206 | let releaseNotesGitCommitFormat = ("",release.Notes |> Seq.map(sprintf "* %s\n")) |> String.Join 207 | 208 | StageAll "" 209 | Git.Commit.Commit "" (sprintf "Bump version to %s \n%s" release.NugetVersion releaseNotesGitCommitFormat) 210 | Branches.push "" 211 | 212 | Branches.tag "" release.NugetVersion 213 | Branches.pushTag "" "origin" release.NugetVersion 214 | ) 215 | 216 | "Clean" ?=> "DotnetRestore" 217 | "Clean" ==> "DotnetPack" 218 | 219 | "DotnetRestore" ?=> "AssemblyInfo" 220 | "AssemblyInfo" ?=> "DotnetBuild" 221 | "AssemblyInfo" ==> "Publish" 222 | 223 | "DotnetRestore" 224 | ==> "DotnetBuild" 225 | ==> "DotnetTest" 226 | ==> "DotnetPack" 227 | ==> "Publish" 228 | ==> "Release" 229 | 230 | "DotnetRestore" 231 | ==> "WatchTests" 232 | 233 | RunTargetOrDefault "DotnetPack" 234 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | cd "$(dirname "$0")" 6 | 7 | PAKET_EXE=.paket/paket.exe 8 | FAKE_EXE=packages/build/FAKE/tools/FAKE.exe 9 | 10 | FSIARGS="" 11 | FSIARGS2="" 12 | OS=${OS:-"unknown"} 13 | 14 | echo $OSTYPE 15 | if [ "$OS" != "Windows_NT" ] 16 | then 17 | # Can't use FSIARGS="--fsiargs -d:MONO" in zsh, so split it up 18 | # (Can't use arrays since dash can't handle them) 19 | FSIARGS="--fsiargs" 20 | FSIARGS2="-d:MONO" 21 | # Allows NETFramework like net45 to be built using dotnet core tooling with mono 22 | export FrameworkPathOverride=$(dirname $(which mono))/../lib/mono/4.5/ 23 | 24 | fi 25 | 26 | run() { 27 | if [ "$OS" != "Windows_NT" ] 28 | then 29 | mono "$@" 30 | else 31 | "$@" 32 | fi 33 | } 34 | 35 | yesno() { 36 | # NOTE: Defaults to NO 37 | read -p "$1 [y/N] " ynresult 38 | case "$ynresult" in 39 | [yY]*) true ;; 40 | *) false ;; 41 | esac 42 | } 43 | 44 | set +e 45 | run $PAKET_EXE restore 46 | exit_code=$? 47 | set -e 48 | 49 | if [ "$OS" != "Windows_NT" ] && 50 | [ $exit_code -ne 0 ] && 51 | [ $(certmgr -list -c Trust | grep X.509 | wc -l) -le 1 ] && 52 | [ $(certmgr -list -c -m Trust | grep X.509 | wc -l) -le 1 ] 53 | then 54 | echo "Your Mono installation has no trusted SSL root certificates set up." 55 | echo "This may result in the Paket bootstrapper failing to download Paket" 56 | echo "because Github's SSL certificate can't be verified. One way to fix" 57 | echo "this issue would be to download the list of SSL root certificates" 58 | echo "from the Mozilla project by running the following command:" 59 | echo "" 60 | echo " mozroots --import --sync" 61 | echo "" 62 | echo "This will import over 100 SSL root certificates into your Mono" 63 | echo "certificate repository." 64 | echo "" 65 | if yesno "Run 'mozroots --import --sync' now?" 66 | then 67 | mozroots --import --sync 68 | else 69 | echo "Attempting to continue without running mozroots. This might fail." 70 | fi 71 | # Re-run bootstrapper whether or not the user ran mozroots, because maybe 72 | # they fixed the problem in a separate terminal window. 73 | run $PAKET_EXE restore 74 | fi 75 | 76 | 77 | 78 | run $FAKE_EXE "$@" $FSIARGS $FSIARGS2 build.fsx 79 | 80 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | .fake/ 2 | obj/ 3 | bin/ 4 | packages/ 5 | paket-files/ 6 | node_modules/ 7 | src/Client/public/ -------------------------------------------------------------------------------- /example/.paket/Paket.Restore.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 8 | 9 | true 10 | $(MSBuildThisFileDirectory) 11 | $(MSBuildThisFileDirectory)..\ 12 | $(PaketRootPath)paket-files\paket.restore.cached 13 | $(PaketRootPath)paket.lock 14 | /Library/Frameworks/Mono.framework/Commands/mono 15 | mono 16 | 17 | $(PaketRootPath)paket.exe 18 | $(PaketToolsPath)paket.exe 19 | "$(PaketExePath)" 20 | $(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 21 | 22 | 23 | <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)")) 24 | dotnet "$(PaketExePath)" 25 | 26 | 27 | "$(PaketExePath)" 28 | 29 | $(PaketRootPath)paket.bootstrapper.exe 30 | $(PaketToolsPath)paket.bootstrapper.exe 31 | "$(PaketBootStrapperExePath)" 32 | $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)" 33 | 34 | 35 | 36 | 37 | true 38 | true 39 | 40 | 41 | 42 | 43 | 44 | 45 | true 46 | $(NoWarn);NU1603 47 | 48 | 49 | 50 | 51 | /usr/bin/shasum $(PaketRestoreCacheFile) | /usr/bin/awk '{ print $1 }' 52 | /usr/bin/shasum $(PaketLockFilePath) | /usr/bin/awk '{ print $1 }' 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) 66 | $([System.IO.File]::ReadAllText('$(PaketLockFilePath)')) 67 | true 68 | false 69 | true 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | $(MSBuildProjectDirectory)\obj\$(MSBuildProjectFile).paket.references.cached 79 | 80 | $(MSBuildProjectFullPath).paket.references 81 | 82 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 83 | 84 | $(MSBuildProjectDirectory)\paket.references 85 | $(MSBuildProjectDirectory)\obj\$(MSBuildProjectFile).$(TargetFramework).paket.resolved 86 | true 87 | references-file-or-cache-not-found 88 | 89 | 90 | 91 | 92 | $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)')) 93 | $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)')) 94 | references-file 95 | false 96 | 97 | 98 | 99 | 100 | false 101 | 102 | 103 | 104 | 105 | true 106 | target-framework '$(TargetFramework)' 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) 124 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) 125 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4]) 126 | 127 | 128 | %(PaketReferencesFileLinesInfo.PackageVersion) 129 | All 130 | 131 | 132 | 133 | 134 | $(MSBuildProjectDirectory)/obj/$(MSBuildProjectFile).paket.clitools 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0]) 144 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1]) 145 | 146 | 147 | %(PaketCliToolFileLinesInfo.PackageVersion) 148 | 149 | 150 | 151 | 155 | 156 | 157 | 158 | 159 | 160 | false 161 | 162 | 163 | 164 | 165 | 166 | <_NuspecFilesNewLocation Include="$(BaseIntermediateOutputPath)$(Configuration)\*.nuspec"/> 167 | 168 | 169 | 170 | $(MSBuildProjectDirectory)/$(MSBuildProjectFile) 171 | true 172 | false 173 | true 174 | $(BaseIntermediateOutputPath)$(Configuration) 175 | $(BaseIntermediateOutputPath) 176 | 177 | 178 | 179 | <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.nuspec"/> 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 232 | 233 | 274 | 275 | 276 | 277 | -------------------------------------------------------------------------------- /example/.paket/paket.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAngryByrd/Hopac.Websockets/35fffb1d9f381b6982a2e34bfcdfb826d0ae85b4/example/.paket/paket.exe -------------------------------------------------------------------------------- /example/build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | cls 3 | 4 | IF EXIST "paket.lock" ( 5 | .paket\paket.exe restore 6 | ) ELSE ( 7 | .paket\paket.exe install 8 | ) 9 | 10 | if errorlevel 1 ( 11 | exit /b %errorlevel% 12 | ) 13 | 14 | packages\build\FAKE\tools\FAKE.exe build.fsx %* 15 | -------------------------------------------------------------------------------- /example/build.fsx: -------------------------------------------------------------------------------- 1 | #r @"packages/build/FAKE/tools/FakeLib.dll" 2 | 3 | open System 4 | 5 | open Fake 6 | 7 | let serverPath = "./src/Server" |> FullName 8 | let clientPath = "./src/Client" |> FullName 9 | let deployDir = "./deploy" |> FullName 10 | 11 | let platformTool tool winTool = 12 | let tool = if isUnix then tool else winTool 13 | tool 14 | |> ProcessHelper.tryFindFileOnPath 15 | |> function Some t -> t | _ -> failwithf "%s not found" tool 16 | 17 | let nodeTool = platformTool "node" "node.exe" 18 | let yarnTool = platformTool "yarn" "yarn.cmd" 19 | 20 | let dotnetcliVersion = DotNetCli.GetDotNetSDKVersionFromGlobalJson() 21 | let mutable dotnetCli = "dotnet" 22 | 23 | let run cmd args workingDir = 24 | let result = 25 | ExecProcess (fun info -> 26 | info.FileName <- cmd 27 | info.WorkingDirectory <- workingDir 28 | info.Arguments <- args) TimeSpan.MaxValue 29 | if result <> 0 then failwithf "'%s %s' failed" cmd args 30 | 31 | Target "Clean" (fun _ -> 32 | CleanDirs [deployDir] 33 | ) 34 | 35 | Target "InstallDotNetCore" (fun _ -> 36 | dotnetCli <- DotNetCli.InstallDotNetSDK dotnetcliVersion 37 | ) 38 | 39 | Target "InstallClient" (fun _ -> 40 | printfn "Node version:" 41 | run nodeTool "--version" __SOURCE_DIRECTORY__ 42 | printfn "Yarn version:" 43 | run yarnTool "--version" __SOURCE_DIRECTORY__ 44 | run yarnTool "install --frozen-lockfile" __SOURCE_DIRECTORY__ 45 | run dotnetCli "restore" clientPath 46 | ) 47 | 48 | Target "RestoreServer" (fun () -> 49 | run dotnetCli "restore" serverPath 50 | ) 51 | 52 | Target "Build" (fun () -> 53 | run dotnetCli "build" serverPath 54 | run dotnetCli "fable webpack -- -p" clientPath 55 | ) 56 | 57 | Target "Run" (fun () -> 58 | let server = async { 59 | run dotnetCli "watch run" serverPath 60 | } 61 | let client = async { 62 | run dotnetCli "fable webpack-dev-server" clientPath 63 | } 64 | let browser = async { 65 | Threading.Thread.Sleep 5000 66 | Diagnostics.Process.Start "http://localhost:8080" |> ignore 67 | } 68 | 69 | [ server; client; browser] 70 | |> Async.Parallel 71 | |> Async.RunSynchronously 72 | |> ignore 73 | ) 74 | 75 | 76 | "Clean" 77 | ==> "InstallDotNetCore" 78 | ==> "InstallClient" 79 | ==> "Build" 80 | 81 | "InstallClient" 82 | ==> "RestoreServer" 83 | ==> "Run" 84 | 85 | RunTargetOrDefault "Build" -------------------------------------------------------------------------------- /example/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | cd "$(dirname "$0")" 6 | 7 | PAKET_EXE=.paket/paket.exe 8 | FAKE_EXE=packages/build/FAKE/tools/FAKE.exe 9 | 10 | FSIARGS="" 11 | FSIARGS2="" 12 | OS=${OS:-"unknown"} 13 | if [ "$OS" != "Windows_NT" ] 14 | then 15 | # Can't use FSIARGS="--fsiargs -d:MONO" in zsh, so split it up 16 | # (Can't use arrays since dash can't handle them) 17 | FSIARGS="--fsiargs" 18 | FSIARGS2="-d:MONO" 19 | fi 20 | 21 | run() { 22 | if [ "$OS" != "Windows_NT" ] 23 | then 24 | mono "$@" 25 | else 26 | "$@" 27 | fi 28 | } 29 | 30 | echo "Executing Paket..." 31 | 32 | FILE='paket.lock' 33 | if [ -f $FILE ]; then 34 | echo "paket.lock file found, restoring packages..." 35 | run $PAKET_EXE restore 36 | else 37 | echo "paket.lock was not found, installing packages..." 38 | run $PAKET_EXE install 39 | fi 40 | 41 | run $FAKE_EXE "$@" $FSIARGS $FSIARGS2 build.fsx 42 | 43 | -------------------------------------------------------------------------------- /example/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "2.1.3" 4 | } 5 | } -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "babel-polyfill": "6.26.0", 5 | "babel-runtime": "6.26.0", 6 | "react": "16.0.0", 7 | "react-bootstrap": "0.31.3", 8 | "react-dom": "16.0.0", 9 | "remotedev": "0.2.7" 10 | }, 11 | "devDependencies": { 12 | "babel-core": "6.26.0", 13 | "babel-loader": "7.1.2", 14 | "babel-plugin-transform-runtime": "6.23.0", 15 | "babel-preset-env": "1.6.0", 16 | "concurrently": "3.5.0", 17 | "fable-loader": "1.1.3", 18 | "fable-utils": "1.0.6", 19 | "webpack": "3.7.1", 20 | "webpack-dev-server": "2.9.1" 21 | }, 22 | "scripts": { 23 | "prebuildServer": "dotnet restore src/Server/Server.fsproj", 24 | "buildServer": "dotnet build src/Server/Server.fsproj", 25 | "prebuildServerTest": "dotnet restore test/ServerTests/ServerTests.fsproj", 26 | "buildServerTest": "dotnet build test/ServerTests/ServerTests.fsproj", 27 | "restoreClient": "cd src/Client && yarn install", 28 | "restoreNetClient": "dotnet restore src/Client/Client.fsproj", 29 | "prestartClient": "concurrently \"npm run restoreClient\" \"npm run restoreNetClient\" ", 30 | "startClient": "cd src/Client && dotnet fable webpack-dev-server" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/paket.dependencies: -------------------------------------------------------------------------------- 1 | group Server 2 | storage: none 3 | source https://api.nuget.org/v3/index.json 4 | 5 | nuget FSharp.Core 6 | nuget Giraffe ~> 1 7 | nuget Microsoft.AspNetCore 8 | nuget Microsoft.AspNetCore.StaticFiles 9 | nuget Microsoft.AspNetCore.WebSockets 10 | nuget Newtonsoft.Json 11 | nuget Fable.JsonConverter 12 | clitool Microsoft.DotNet.Watcher.Tools 13 | 14 | group Client 15 | storage: none 16 | source https://api.nuget.org/v3/index.json 17 | 18 | nuget Fable.Core 19 | nuget Fable.Elmish.Debugger 20 | nuget Fable.Elmish.React 21 | nuget Fable.Elmish.HMR 22 | clitool dotnet-fable 23 | 24 | group Build 25 | source https://api.nuget.org/v3/index.json 26 | 27 | nuget FAKE 28 | -------------------------------------------------------------------------------- /example/src/Client/Client.fs: -------------------------------------------------------------------------------- 1 | module Client 2 | 3 | open Elmish 4 | open Elmish.React 5 | 6 | open Fable.Core 7 | open Fable.Core.JsInterop 8 | open Fable.Import 9 | open Fable.Import.Browser 10 | open Fable.Helpers.React 11 | open Fable.Helpers.React.Props 12 | open Fable.PowerPack.Fetch 13 | 14 | open Shared 15 | 16 | 17 | 18 | type Model = { 19 | Counter : Counter option 20 | Ticker : Ticker option } 21 | 22 | type Msg = 23 | | Increment 24 | | Decrement 25 | | InitCount of Result 26 | | NewTick of Ticker 27 | 28 | 29 | 30 | let init () = 31 | let model = { 32 | Counter = None 33 | Ticker = None 34 | } 35 | let cmd = 36 | Cmd.ofPromise 37 | (fetchAs "/api/init") 38 | [] 39 | (Ok >> InitCount) 40 | (Error >> InitCount) 41 | model, cmd 42 | 43 | let update msg (model : Model) = 44 | let model' = 45 | match model.Counter, msg with 46 | | Some x, Increment -> { model with Counter = Some (x + 1) } 47 | | Some x, Decrement -> { model with Counter = Some (x - 1) } 48 | | None, InitCount (Ok x) -> 49 | { model with Counter = Some x } 50 | | _, NewTick tick -> { model with Ticker = Some tick } 51 | | _ -> { model with Counter = None } 52 | model', Cmd.none 53 | 54 | let safeComponents = 55 | let intersperse sep ls = 56 | List.foldBack (fun x -> function 57 | | [] -> [x] 58 | | xs -> x::sep::xs) ls [] 59 | 60 | let components = 61 | [ 62 | "Giraffe", "https://github.com/giraffe-fsharp/Giraffe" 63 | "Fable", "http://fable.io" 64 | "Elmish", "https://fable-elmish.github.io/" 65 | ] 66 | |> List.map (fun (desc,link) -> a [ Href link ] [ str desc ] ) 67 | |> intersperse (str ", ") 68 | |> span [ ] 69 | 70 | p [ ] 71 | [ strong [] [ str "SAFE Template" ] 72 | str " powered by: " 73 | components ] 74 | 75 | let show = function 76 | | Some x -> string x 77 | | None -> "Loading..." 78 | 79 | let showTick = function 80 | | Some x -> string x 81 | | None -> "Loading ticker..." 82 | 83 | let view model dispatch = 84 | div [] 85 | [ h1 [] [ str "SAFE Template" ] 86 | p [] [ str "The initial counter is fetched from server" ] 87 | p [] [ str "Press buttons to manipulate counter:" ] 88 | p [] [ str (showTick model.Ticker)] 89 | button [ OnClick (fun _ -> dispatch Decrement) ] [ str "-" ] 90 | div [] [ str (show model.Counter) ] 91 | button [ OnClick (fun _ -> dispatch Increment) ] [ str "+" ] 92 | safeComponents ] 93 | 94 | 95 | let websocket = WebSocket.Create((sprintf "ws://%s/ws/tickerWS" window.location.host)) 96 | 97 | let webSocketSub initial = 98 | let sub dispatch = 99 | websocket.addEventListener_message(fun event -> 100 | unbox event.data |> NewTick |> dispatch 101 | null 102 | ) |> ignore 103 | Cmd.ofSub sub 104 | 105 | 106 | 107 | #if DEBUG 108 | open Elmish.Debug 109 | open Elmish.HMR 110 | #endif 111 | 112 | Program.mkProgram init update view 113 | |> Program.withSubscription webSocketSub 114 | #if DEBUG 115 | |> Program.withConsoleTrace 116 | |> Program.withHMR 117 | #endif 118 | |> Program.withReact "elmish-app" 119 | #if DEBUG 120 | |> Program.withDebugger 121 | #endif 122 | |> Program.run 123 | -------------------------------------------------------------------------------- /example/src/Client/Client.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/src/Client/Images/safe_favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAngryByrd/Hopac.Websockets/35fffb1d9f381b6982a2e34bfcdfb826d0ae85b4/example/src/Client/Images/safe_favicon.png -------------------------------------------------------------------------------- /example/src/Client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SAFE Template 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /example/src/Client/paket.references: -------------------------------------------------------------------------------- 1 | group Client 2 | FSharp.Core 3 | Fable.Elmish.Debugger 4 | Fable.Elmish.React 5 | Fable.Elmish.HMR 6 | Fable.Core 7 | dotnet-fable 8 | -------------------------------------------------------------------------------- /example/src/Client/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var webpack = require("webpack"); 3 | var fableUtils = require("fable-utils"); 4 | 5 | function resolve(filePath) { 6 | return path.join(__dirname, filePath) 7 | } 8 | 9 | var babelOptions = fableUtils.resolveBabelOptions({ 10 | presets: [ 11 | ["env", { 12 | "targets": { 13 | "browsers": ["last 2 versions"] 14 | }, 15 | "modules": false 16 | }] 17 | ], 18 | plugins: ["transform-runtime"] 19 | }); 20 | 21 | 22 | var isProduction = process.argv.indexOf("-p") >= 0; 23 | var port = process.env.SUAVE_FABLE_PORT || "8085"; 24 | console.log("Bundling for " + (isProduction ? "production" : "development") + "..."); 25 | 26 | module.exports = { 27 | devtool: "source-map", 28 | entry: resolve('./Client.fsproj'), 29 | output: { 30 | path: resolve('./public'), 31 | publicPath: "/public", 32 | filename: "bundle.js" 33 | }, 34 | resolve: { 35 | modules: [resolve("../../node_modules/")] 36 | }, 37 | devServer: { 38 | proxy: { 39 | '/api/*': { 40 | target: 'http://localhost:' + port, 41 | changeOrigin: true 42 | }, 43 | '/ws/*': { 44 | target: 'ws://localhost:' + port, 45 | "ws": true 46 | }, 47 | }, 48 | hot: true, 49 | inline: true 50 | }, 51 | module: { 52 | rules: [{ 53 | test: /\.fs(x|proj)?$/, 54 | use: { 55 | loader: "fable-loader", 56 | options: { 57 | babel: babelOptions, 58 | define: isProduction ? [] : ["DEBUG"] 59 | } 60 | } 61 | }, { 62 | test: /\.js$/, 63 | exclude: /node_modules/, 64 | use: { 65 | loader: 'babel-loader', 66 | options: babelOptions 67 | }, 68 | }] 69 | }, 70 | plugins: isProduction ? [] : [ 71 | new webpack.HotModuleReplacementPlugin(), 72 | new webpack.NamedModulesPlugin() 73 | ] 74 | }; -------------------------------------------------------------------------------- /example/src/Server/Server.fs: -------------------------------------------------------------------------------- 1 | open System 2 | open System.IO 3 | open System.Net.WebSockets 4 | open System.Threading.Tasks 5 | 6 | open Microsoft.AspNetCore 7 | open Microsoft.AspNetCore.Builder 8 | open Microsoft.AspNetCore.Hosting 9 | open Microsoft.Extensions.DependencyInjection 10 | open Newtonsoft.Json 11 | open Giraffe 12 | open Hopac 13 | open Hopac.Websockets 14 | open Hopac.Websockets.AspNetCore 15 | 16 | open Shared 17 | 18 | 19 | let clientPath = Path.Combine("..","Client") |> Path.GetFullPath 20 | let port = 8085us 21 | 22 | let getInitCounter () : Task = task { return 42 } 23 | 24 | type Dependencies = { 25 | TickerStream : Hopac.Stream 26 | } 27 | 28 | let webApp (dependencies: Dependencies ): HttpHandler = 29 | choose [ 30 | route "/api/init" >=> 31 | fun next ctx -> 32 | task { 33 | let! counter = getInitCounter() 34 | return! Successful.OK counter next ctx 35 | } 36 | route "/ws/tickerWS" >=> 37 | fun next ctx -> 38 | task { 39 | if ctx.WebSockets.IsWebSocketRequest then 40 | printfn "Received websocket request!" 41 | let finished = IVar () 42 | job { 43 | try 44 | let! threadSafeWebSocket = ctx.WebSockets.AcceptThreadSafeWebsocket() 45 | printfn "Connected websocket request!" 46 | while threadSafeWebSocket.websocket.State <> WebSocketState.Closed do 47 | do! 48 | dependencies.TickerStream 49 | |> Stream.mapFun JsonConvert.SerializeObject 50 | |> Stream.iterJob (ThreadSafeWebSocket.sendMessageAsUTF8 threadSafeWebSocket) 51 | with e -> 52 | printfn "sendMessageAsUTF8 error %A" e 53 | do! IVar.tryFill finished () 54 | } |> start 55 | job { 56 | try 57 | let! threadSafeWebSocket = ctx.WebSockets.AcceptThreadSafeWebsocket() 58 | printfn "Connected websocket request!" 59 | while threadSafeWebSocket.websocket.State <> WebSocketState.Closed do 60 | let! result = ThreadSafeWebSocket.receiveMessageAsUTF8 threadSafeWebSocket 61 | printfn "received msg %s" result 62 | with e -> 63 | printfn "receiveMessageAsUTF8 error %A" e 64 | do! IVar.tryFill finished () 65 | } |> start 66 | do! finished |> startAsTask 67 | return! Successful.ok (text "OK") next ctx 68 | else 69 | return! next ctx 70 | } 71 | ] 72 | 73 | 74 | 75 | 76 | let configureApp dependencies (app : IApplicationBuilder) = 77 | app.UseStaticFiles() 78 | .UseWebSockets() 79 | .UseGiraffe (webApp dependencies) 80 | 81 | 82 | 83 | open Hopac.Stream 84 | 85 | let ticker () = 86 | let src = Stream.Src.create() 87 | 88 | let looper index = job { 89 | do! timeOutMillis 1000 90 | let ticker = { 91 | Timestamp = DateTimeOffset.UtcNow 92 | Value = index 93 | Name = "Foo" 94 | } 95 | do! Stream.Src.value src ticker 96 | return index + 1 97 | } 98 | 99 | looper 100 | |> Job.iterateServer 0 101 | |> start 102 | 103 | Stream.Src.tap src 104 | 105 | 106 | let configureServices (services : IServiceCollection) = 107 | services.AddGiraffe() |> ignore 108 | 109 | let dependencies = { 110 | TickerStream = ticker () 111 | } 112 | 113 | JsonConvert.DefaultSettings <- fun () -> 114 | let setttings = JsonSerializerSettings() 115 | setttings.Converters.Add(Fable.JsonConverter()) 116 | setttings 117 | 118 | WebHost 119 | .CreateDefaultBuilder() 120 | .UseWebRoot(clientPath) 121 | .UseContentRoot(clientPath) 122 | .Configure(Action (configureApp dependencies)) 123 | .ConfigureServices(configureServices) 124 | .UseUrls("http://0.0.0.0:" + port.ToString() + "/") 125 | .Build() 126 | .Run() 127 | -------------------------------------------------------------------------------- /example/src/Server/Server.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/src/Server/paket.references: -------------------------------------------------------------------------------- 1 | group Server 2 | FSharp.Core 3 | Giraffe 4 | Microsoft.AspNetCore 5 | Microsoft.AspNetCore.StaticFiles 6 | Microsoft.DotNet.Watcher.Tools 7 | Microsoft.AspNetCore.WebSockets 8 | Newtonsoft.Json 9 | Fable.JsonConverter 10 | -------------------------------------------------------------------------------- /example/src/Shared/Shared.fs: -------------------------------------------------------------------------------- 1 | namespace Shared 2 | 3 | open System 4 | type Counter = int 5 | 6 | type Ticker = { 7 | Timestamp : DateTimeOffset 8 | Value : int 9 | Name : string 10 | } 11 | -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | source https://www.nuget.org/api/v2 2 | storage: none 3 | clitool dotnet-mono 0.5.2 4 | clitool Microsoft.DotNet.Watcher.Tools 1.0.0 5 | nuget Argu 3.7 6 | nuget FSharp.Core 4.3.3 7 | nuget Hopac 8 | nuget SourceLink.Create.CommandLine 2.7.2 copy_local: true 9 | 10 | #test 11 | nuget Expecto 6.0.0 12 | nuget Microsoft.AspNetCore.TestHost 13 | nuget Microsoft.AspNetCore.WebSockets 14 | nuget Microsoft.AspNetCore.Server.Kestrel 15 | 16 | group Build 17 | source https://www.nuget.org/api/v2 18 | nuget FAKE -------------------------------------------------------------------------------- /src/Hopac.Websockets.AspNetCore/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | // Auto-Generated by FAKE; do not edit 2 | namespace System 3 | open System.Reflection 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | [] 11 | [] 12 | [] 13 | do () 14 | 15 | module internal AssemblyVersionInformation = 16 | let [] AssemblyTitle = "Hopac.Websockets.AspNetCore" 17 | let [] AssemblyProduct = "Hopac.Websockets" 18 | let [] AssemblyVersion = "0.4.0" 19 | let [] AssemblyMetadata_ReleaseDate = "2018-05-16T00:00:00.0000000" 20 | let [] AssemblyFileVersion = "0.4.0" 21 | let [] AssemblyInformationalVersion = "0.4.0" 22 | let [] AssemblyMetadata_ReleaseChannel = "release" 23 | let [] AssemblyMetadata_GitHash = "04e38375fd8e8ee3a17d7be41a7fdf900469d9ae" 24 | -------------------------------------------------------------------------------- /src/Hopac.Websockets.AspNetCore/Hopac.Websockets.AspNetCore.fs: -------------------------------------------------------------------------------- 1 | namespace Hopac.Websockets.AspNetCore 2 | 3 | [] 4 | module Library = 5 | open Hopac 6 | open Hopac.Infixes 7 | open Hopac.Websockets 8 | type Microsoft.AspNetCore.Http.WebSocketManager with 9 | /// Transitions the request to a ThreadSafeWebSocket connection 10 | member this.AcceptThreadSafeWebsocket() = 11 | Job.fromTask(this.AcceptWebSocketAsync) 12 | >>= ThreadSafeWebSocket.createFromWebSocket 13 | 14 | /// Transitions the request to a ThreadSafeWebSocket connection using the specified sub-protocol. 15 | member this.AcceptThreadSafeWebsocket(subprotocol : string) = 16 | Job.fromTask(fun () -> this.AcceptWebSocketAsync subprotocol) 17 | >>= ThreadSafeWebSocket.createFromWebSocket 18 | 19 | -------------------------------------------------------------------------------- /src/Hopac.Websockets.AspNetCore/Hopac.Websockets.AspNetCore.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Hopac.Websockets.AspNetCore 5 | Hopac.Websockets.AspNetCore - AspNetCore extensions for Hopac.Websockets 6 | 7 | f#, fsharp 8 | https://github.com/TheAngryByrd/Hopac.Websockets 9 | https://github.com/TheAngryByrd/Hopac.Websockets/blob/master/LICENSE.md 10 | false 11 | git 12 | TheAngryByrd 13 | https://github.com/TheAngryByrd/Hopac.Websockets 14 | 15 | 16 | 17 | true 18 | true 19 | 20 | 21 | netstandard2.0 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Hopac.Websockets.AspNetCore/paket.references: -------------------------------------------------------------------------------- 1 | SourceLink.Create.CommandLine 2 | Microsoft.AspNetCore.Http.Abstractions 3 | -------------------------------------------------------------------------------- /src/Hopac.Websockets/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | // Auto-Generated by FAKE; do not edit 2 | namespace System 3 | open System.Reflection 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | [] 11 | [] 12 | [] 13 | do () 14 | 15 | module internal AssemblyVersionInformation = 16 | let [] AssemblyTitle = "Hopac.Websockets" 17 | let [] AssemblyProduct = "Hopac.Websockets" 18 | let [] AssemblyVersion = "0.4.0" 19 | let [] AssemblyMetadata_ReleaseDate = "2018-05-16T00:00:00.0000000" 20 | let [] AssemblyFileVersion = "0.4.0" 21 | let [] AssemblyInformationalVersion = "0.4.0" 22 | let [] AssemblyMetadata_ReleaseChannel = "release" 23 | let [] AssemblyMetadata_GitHash = "04e38375fd8e8ee3a17d7be41a7fdf900469d9ae" 24 | -------------------------------------------------------------------------------- /src/Hopac.Websockets/Hopac.Websockets.fs: -------------------------------------------------------------------------------- 1 | namespace Hopac.Websockets 2 | 3 | open System 4 | open Hopac 5 | [] 6 | module Infixes = 7 | let (^) = (<|) 8 | 9 | 10 | module Hopac = 11 | open System 12 | open Hopac 13 | open Hopac.Infixes 14 | open System.Threading 15 | module Alt = 16 | let using (disposable : #IDisposable) (alt : #IDisposable -> #Alt<'a>) = 17 | alt disposable 18 | |> fun a -> Alt.tryFinallyFun a disposable.Dispose 19 | 20 | let fromCT (ct : CancellationToken) = 21 | let cancelled = IVar() 22 | using 23 | (ct.Register(fun () -> cancelled *<= () |> start)) 24 | ^ fun _ -> cancelled 25 | 26 | module Infixes = 27 | let ( *<-->= ) qCh rCh2n2qJ = Alt.withNackJob <| fun nack -> 28 | let rCh = IVar<_> () 29 | rCh2n2qJ rCh nack >>= fun q -> 30 | qCh *<+ q >>-. 31 | rCh 32 | 33 | let ( *<-->- ) qCh rCh2n2q = 34 | qCh *<-->= fun rCh n -> rCh2n2q rCh n |> Job.result 35 | [] 36 | module Stream = 37 | open System 38 | open Hopac 39 | 40 | let read buffer offset count (stream : #IO.Stream) = 41 | Alt.fromTask ^ fun ct -> 42 | stream.ReadAsync(buffer, offset, count, ct) 43 | 44 | let write buffer offset count (stream : #IO.Stream) = 45 | Alt.fromUnitTask ^ fun ct -> 46 | stream.WriteAsync(buffer, offset, count, ct) 47 | 48 | type System.IO.Stream with 49 | member stream.ReadJob (buffer: byte[], ?offset, ?count) = 50 | let offset = defaultArg offset 0 51 | let count = defaultArg count buffer.Length 52 | read buffer offset count stream 53 | 54 | member stream.WriteJob (buffer: byte[], ?offset, ?count) = 55 | let offset = defaultArg offset 0 56 | let count = defaultArg count buffer.Length 57 | write buffer offset count stream 58 | 59 | type System.IO.MemoryStream with 60 | static member UTF8toMemoryStream (text : string) = 61 | new IO.MemoryStream(Text.Encoding.UTF8.GetBytes text) 62 | 63 | static member ToUTF8String (stream : IO.MemoryStream) = 64 | stream.Seek(0L,IO.SeekOrigin.Begin) |> ignore //ensure start of stream 65 | stream.ToArray() 66 | |> Text.Encoding.UTF8.GetString 67 | |> fun s -> s.TrimEnd(char 0) 68 | 69 | member stream.ToUTF8String () = 70 | stream |> System.IO.MemoryStream.ToUTF8String 71 | 72 | 73 | 74 | 75 | module WebSocket = 76 | open System 77 | open Hopac 78 | open System.Net.WebSockets 79 | open Hopac.Infixes 80 | 81 | /// Size of the buffer used when sending messages over the socket 82 | type BufferSize = int 83 | 84 | /// (16 * 1024) = 16384 85 | /// https://referencesource.microsoft.com/#System/net/System/Net/WebSockets/WebSocketHelpers.cs,285b8b64a4da6851 86 | [] 87 | let defaultBufferSize : BufferSize = 16384 // (16 * 1024) 88 | 89 | /// A Hopac Alt version of ReceiveAsync 90 | /// Alt: https://hopac.github.io/Hopac/Hopac.html#def:type%20Hopac.Alt 91 | /// ReceiveAsync: https://docs.microsoft.com/en-us/dotnet/api/system.net.websockets.websocket.receiveasync?view=netcore-2.0 92 | let receive (buffer : ArraySegment) (websocket : #WebSocket)= 93 | Alt.fromTask ^ fun ct -> 94 | websocket.ReceiveAsync(buffer,ct) 95 | 96 | /// A Hopac Alt version of SendAsync 97 | /// Alt: https://hopac.github.io/Hopac/Hopac.html#def:type%20Hopac.Alt 98 | /// SendAsync: https://docs.microsoft.com/en-us/dotnet/api/system.net.websockets.websocket.sendasync?view=netcore-2.0 99 | let send (buffer : ArraySegment) messageType endOfMessage (websocket : #WebSocket) = 100 | Alt.fromUnitTask ^ fun ct -> 101 | websocket.SendAsync(buffer, messageType, endOfMessage, ct) 102 | 103 | /// A Hopac Alt version of CloseAsync 104 | /// Grace approach to shutting down. Use when you want to other end to acknowledge the close. 105 | /// Alt: https://hopac.github.io/Hopac/Hopac.html#def:type%20Hopac.Alt 106 | /// CloseAsync: https://docs.microsoft.com/en-us/dotnet/api/system.net.websockets.websocket.closeasync?view=netcore-2.0 107 | let close status message (websocket : #WebSocket) = 108 | Alt.fromUnitTask ^ fun ct -> 109 | websocket.CloseAsync(status,message,ct) 110 | 111 | /// A Hopac Alt version of CloseOutputAsync 112 | /// Hard approach to shutting down. Useful when you don't want the other end to acknowledge the close or this end has received a close notification and want to acknowledge the close. 113 | /// Alt: https://hopac.github.io/Hopac/Hopac.html#def:type%20Hopac.Alt 114 | /// CloseOutputAsync: https://docs.microsoft.com/en-us/dotnet/api/system.net.websockets.websocket.closeoutputasync?view=netcore-2.0 115 | let closeOutput status message (websocket : #WebSocket) = 116 | Alt.fromUnitTask ^ fun ct -> 117 | websocket.CloseOutputAsync(status,message,ct) 118 | 119 | let isWebsocketOpen (socket : #WebSocket) = 120 | socket.State = WebSocketState.Open 121 | 122 | /// Sends a whole message to the websocket read from the given stream 123 | let sendMessage bufferSize messageType (readableStream : #IO.Stream) (socket : #WebSocket) = 124 | Alt.withNackJob ^ fun nack -> 125 | Promise.start ^ job { 126 | let buffer = Array.create (bufferSize) Byte.MinValue 127 | 128 | let rec sendMessage' () = job { 129 | let! read = 130 | readableStream |> Stream.read buffer 0 buffer.Length 131 | <|> nack ^-> fun () -> 0 132 | if read > 0 then 133 | do! 134 | (socket |> send (ArraySegment(buffer |> Array.take read)) messageType false) 135 | <|> nack 136 | return! sendMessage'() 137 | else 138 | do! 139 | (socket |> send (ArraySegment(Array.empty)) messageType true) 140 | <|> nack 141 | } 142 | do! sendMessage'() 143 | } 144 | 145 | /// Sends the UTF8 string as a whole websocket message 146 | let sendMessageAsUTF8 text socket = 147 | Alt.using (IO.MemoryStream.UTF8toMemoryStream text) 148 | ^ fun stream -> 149 | sendMessage defaultBufferSize WebSocketMessageType.Text stream socket 150 | 151 | /// Receives a whole message written to the given stream 152 | /// Attempts to handle closes gracefully 153 | let receiveMessage bufferSize messageType (writeableStream : IO.Stream) (socket : WebSocket) = 154 | Alt.withNackJob ^ fun nack -> 155 | Promise.start ^ job { 156 | let buffer = new ArraySegment( Array.create (bufferSize) Byte.MinValue) 157 | 158 | let rec readTillEnd' () = job { 159 | let! (result : WebSocketReceiveResult option) = 160 | ((socket |> receive buffer) ^-> Some) 161 | <|> (nack ^-> fun _ -> None) 162 | match result with 163 | | Some result when result.MessageType = WebSocketMessageType.Close || socket.State = WebSocketState.CloseReceived -> 164 | // printfn "Close received! %A - %A" socket.CloseStatus socket.CloseStatusDescription 165 | do! closeOutput WebSocketCloseStatus.NormalClosure "Close received" socket 166 | | Some result -> 167 | // printfn "result.MessageType -> %A" result.MessageType 168 | if result.MessageType <> messageType then return () 169 | do! writeableStream |> Stream.write buffer.Array buffer.Offset result.Count 170 | <|> nack 171 | if result.EndOfMessage then 172 | return () 173 | else return! readTillEnd' () 174 | | None -> 175 | return () 176 | } 177 | do! readTillEnd' () 178 | } 179 | 180 | /// Receives a whole message as a utf8 string 181 | let receiveMessageAsUTF8 socket = 182 | Alt.using (new IO.MemoryStream()) 183 | ^ fun stream -> 184 | receiveMessage defaultBufferSize WebSocketMessageType.Text stream socket 185 | ^-> fun _ -> stream |> IO.MemoryStream.ToUTF8String 186 | 187 | 188 | [] 189 | module ThreadSafeWebSocket = 190 | open System 191 | open Hopac 192 | open System.Net.WebSockets 193 | open Hopac.Infixes 194 | 195 | type SendMessage = WebSocket.BufferSize * WebSocketMessageType * IO.Stream * IVar * Promise 196 | type ReceiveMessage=WebSocket.BufferSize * WebSocketMessageType * IO.Stream * IVar * Promise 197 | type CloseMessage = WebSocketCloseStatus * string * IVar * Promise 198 | type CloseOutputMessage = WebSocketCloseStatus * string * IVar * Promise 199 | 200 | type ThreadSafeWebSocket = 201 | { websocket : WebSocket 202 | sendCh : Ch 203 | receiveCh : Ch 204 | closeCh : Ch 205 | closeOutputCh : Ch } 206 | interface IDisposable with 207 | member x.Dispose() = 208 | x.websocket.Dispose() 209 | member x.State = 210 | x.websocket.State 211 | member x.CloseStatus = 212 | x.websocket.CloseStatus |> Option.ofNullable 213 | member x.CloseStatusDescription = 214 | x.websocket.CloseStatusDescription 215 | 216 | /// Websockets only allow for one receive and one send at a time. This results in if multiple threads try to write to a stream, it will throw a `System.InvalidOperationException`. This wraps a websocket in a hopac server-client model that allows for multiple threads to write or read at the same time. 217 | /// See https://docs.microsoft.com/en-us/dotnet/api/system.net.websockets.websocket.receiveasync?view=netcore-2.0#Remarks 218 | module ThreadSafeWebSocket = 219 | /// Creates a threadsafe websocket from already created websocket. 220 | /// Websockets only allow for one receive and one send at a time. This results in if multiple threads try to write to a stream, it will throw a `System.InvalidOperationException`. This wraps a websocket in a hopac server-client model that allows for multiple threads to write or read at the same time. 221 | /// See https://docs.microsoft.com/en-us/dotnet/api/system.net.websockets.websocket.receiveasync?view=netcore-2.0#Remarks 222 | let createFromWebSocket (webSocket : WebSocket) = 223 | 224 | let self = { 225 | websocket = webSocket 226 | sendCh = Ch () 227 | receiveCh = Ch () 228 | closeCh = Ch () 229 | closeOutputCh = Ch () 230 | } 231 | 232 | let send () = 233 | self.sendCh ^=> fun (bufferSize, messageType, stream, reply, nack) -> job { 234 | do! Alt.tryIn 235 | (WebSocket.sendMessage bufferSize messageType stream webSocket) 236 | (IVar.fill reply) 237 | (IVar.fillFailure reply) 238 | <|> nack 239 | } 240 | 241 | let receive () = 242 | self.receiveCh ^=> fun (bufferSize, messageType, stream, reply, nack) -> job { 243 | do! Alt.tryIn 244 | (WebSocket.receiveMessage bufferSize messageType stream webSocket) 245 | (IVar.fill reply) 246 | (IVar.fillFailure reply) 247 | <|> nack 248 | } 249 | 250 | let close () = 251 | self.closeCh ^=> fun (status, message, reply, nack) -> job { 252 | do! Alt.tryIn 253 | (WebSocket.close status message webSocket) 254 | (IVar.fill reply) 255 | (IVar.fillFailure reply) 256 | <|> nack 257 | } 258 | 259 | let closeOutput () = 260 | self.closeOutputCh ^=> fun (status, message, reply, nack) -> job { 261 | do! Alt.tryIn 262 | (WebSocket.closeOutput status message webSocket) 263 | (IVar.fill reply) 264 | (IVar.fillFailure reply) 265 | <|> nack 266 | } 267 | 268 | let receiveProc = Job.delay ^ fun () -> 269 | receive () 270 | let sendProc = Job.delay ^ fun () -> 271 | send () <|> close () <|> closeOutput () 272 | 273 | Job.foreverServer sendProc 274 | >>=. Job.foreverServer receiveProc 275 | >>-. self 276 | 277 | 278 | /// Sends a whole message to the websocket read from the given stream 279 | let sendMessage wsts bufferSize messageType stream = 280 | wsts.sendCh *<-->- fun reply nack -> 281 | (bufferSize, messageType, stream, reply,nack) 282 | 283 | /// Sends the UTF8 string as a whole websocket message 284 | let sendMessageAsUTF8(wsts : ThreadSafeWebSocket) (text : string) = 285 | Alt.using 286 | (IO.MemoryStream.UTF8toMemoryStream text) 287 | ^ fun ms -> sendMessage wsts WebSocket.defaultBufferSize WebSocketMessageType.Text ms 288 | 289 | /// Receives a whole message written to the given stream 290 | /// Attempts to handle closes gracefully 291 | let receiveMessage wsts bufferSize messageType stream = 292 | wsts.receiveCh *<-->- fun reply nack -> 293 | (bufferSize, messageType, stream, reply,nack) 294 | 295 | /// Receives a whole message as a utf8 string 296 | let receiveMessageAsUTF8 (wsts : ThreadSafeWebSocket) = 297 | Alt.using 298 | (new IO.MemoryStream()) 299 | ^ fun stream -> 300 | receiveMessage wsts WebSocket.defaultBufferSize WebSocketMessageType.Text stream 301 | ^-> fun () -> 302 | stream |> IO.MemoryStream.ToUTF8String //Remove null terminator 303 | 304 | /// Grace approach to shutting down. Use when you want to other end to acknowledge the close. 305 | /// CloseAsync: https://docs.microsoft.com/en-us/dotnet/api/system.net.websockets.websocket.closeasync?view=netcore-2.0 306 | let close wsts status message = 307 | wsts.closeCh *<-->- fun reply nack -> 308 | (status, message, reply, nack) 309 | 310 | 311 | /// Hard approach to shutting down. Useful when you don't want the other end to acknowledge the close or this end has received a close notification and want to acknowledge the close. 312 | /// Alt: https://hopac.github.io/Hopac/Hopac.html#def:type%20Hopac.Alt 313 | /// CloseOutputAsync: https://docs.microsoft.com/en-us/dotnet/api/system.net.websockets.websocket.closeoutputasync?view=netcore-2.0 314 | let closeOutput wsts status message = 315 | wsts.closeOutputCh *<-->- fun reply nack -> 316 | (status, message, reply, nack) 317 | -------------------------------------------------------------------------------- /src/Hopac.Websockets/Hopac.Websockets.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net461 5 | 6 | 7 | Hopac.Websockets 8 | Hopac.Websockets - A Threadsafe Hopac wrapper around Websockets 9 | 10 | f#, fsharp 11 | https://github.com/TheAngryByrd/Hopac.Websockets 12 | https://github.com/TheAngryByrd/Hopac.Websockets/blob/master/LICENSE.md 13 | false 14 | git 15 | TheAngryByrd 16 | https://github.com/TheAngryByrd/Hopac.Websockets 17 | 18 | 19 | 20 | true 21 | true 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Hopac.Websockets/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | SourceLink.Create.CommandLine 3 | Hopac -------------------------------------------------------------------------------- /tests/Hopac.Websockets.Tests/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/Hopac.Websockets.Tests/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | // Auto-Generated by FAKE; do not edit 2 | namespace System 3 | open System.Reflection 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | [] 11 | [] 12 | [] 13 | do () 14 | 15 | module internal AssemblyVersionInformation = 16 | let [] AssemblyTitle = "Hopac.Websockets.Tests" 17 | let [] AssemblyProduct = "Hopac.Websockets" 18 | let [] AssemblyVersion = "0.4.0" 19 | let [] AssemblyMetadata_ReleaseDate = "2018-05-16T00:00:00.0000000" 20 | let [] AssemblyFileVersion = "0.4.0" 21 | let [] AssemblyInformationalVersion = "0.4.0" 22 | let [] AssemblyMetadata_ReleaseChannel = "release" 23 | let [] AssemblyMetadata_GitHash = "04e38375fd8e8ee3a17d7be41a7fdf900469d9ae" 24 | -------------------------------------------------------------------------------- /tests/Hopac.Websockets.Tests/Hopac.Websockets.Tests.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.0;net461 6 | true 7 | true 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/Hopac.Websockets.Tests/Main.fs: -------------------------------------------------------------------------------- 1 | module ExpectoTemplate 2 | open Expecto 3 | open System.Reflection 4 | open System.Threading.Tasks 5 | 6 | 7 | module AssemblyInfo = 8 | 9 | let metaDataValue (mda : AssemblyMetadataAttribute) = mda.Value 10 | let getMetaDataAttribute (assembly : Assembly) key = 11 | assembly.GetCustomAttributes(typedefof) 12 | |> Seq.cast 13 | |> Seq.find(fun x -> x.Key = key) 14 | 15 | let getReleaseDate assembly = 16 | "ReleaseDate" 17 | |> getMetaDataAttribute assembly 18 | |> metaDataValue 19 | 20 | let getGitHash assembly = 21 | "GitHash" 22 | |> getMetaDataAttribute assembly 23 | |> metaDataValue 24 | 25 | [] 26 | let main argv = 27 | if argv |> Seq.contains ("--version") then 28 | let assembly = Assembly.GetEntryAssembly() 29 | let name = assembly.GetName() 30 | let version = assembly.GetName().Version 31 | let releaseDate = AssemblyInfo.getReleaseDate assembly 32 | let githash = AssemblyInfo.getGitHash assembly 33 | printfn "%s - %A - %s - %s" name.Name version releaseDate githash 34 | let config = { 35 | defaultConfig with 36 | ``parallel`` = false 37 | } 38 | Tests.runTestsInAssembly config argv 39 | -------------------------------------------------------------------------------- /tests/Hopac.Websockets.Tests/Tests.fs: -------------------------------------------------------------------------------- 1 | module Tests 2 | 3 | 4 | open Hopac.Websockets 5 | open Expecto 6 | open Hopac 7 | open System 8 | open System.Net 9 | open System.Net.WebSockets 10 | open System.Threading 11 | open System.Threading.Tasks 12 | open Microsoft.AspNetCore.Builder 13 | open Microsoft.AspNetCore.Hosting 14 | open Microsoft.AspNetCore.Http 15 | open Microsoft.AspNetCore.TestHost 16 | open Microsoft.Extensions.Configuration 17 | open Hopac.Websockets 18 | open Expecto.Logging 19 | 20 | 21 | [] 22 | module Expecto = 23 | let testCaseJob name (job : Job) = testCaseAsync name (job |> Job.toAsync) 24 | 25 | module Expect = 26 | let exceptionEquals exType message (ex : exn) = 27 | let actualExType = ex.GetType().ToString() 28 | if exType <> actualExType then 29 | Tests.failtestf "Expected exception of %s but got %s" exType actualExType 30 | if ex.Message <> message then 31 | Tests.failtestf "Expected message %s but got %s" message ex.Message 32 | 33 | let exceptionExists exType message (exns : exn seq) = 34 | let exnTypes = 35 | exns 36 | |> Seq.map ^ fun ex -> ex.GetType().ToString() 37 | Expect.contains exnTypes exType "No exception matching that type found" 38 | 39 | let exnMessages = 40 | exns 41 | |> Seq.map ^ fun ex -> ex.Message 42 | Expect.contains exnMessages message "No exception message matching that string found" 43 | 44 | let random = Random(42) 45 | let genStr = 46 | let chars = "ABCDEFGHIJKLMNOPQRSTUVWUXYZ0123456789" 47 | let charsLen = chars.Length 48 | 49 | 50 | fun len -> 51 | let randomChars = [|for _ in 0..len -> chars.[random.Next(charsLen)]|] 52 | new string(randomChars) 53 | 54 | let echoWebSocket (httpContext : HttpContext) (next : unit -> Job) = job { 55 | if httpContext.WebSockets.IsWebSocketRequest then 56 | let! (websocket : WebSocket) = httpContext.WebSockets.AcceptWebSocketAsync() 57 | while websocket.State <> WebSocketState.Closed do 58 | do! websocket 59 | |> WebSocket.receiveMessageAsUTF8 60 | |> Job.bind(fun text -> WebSocket.sendMessageAsUTF8 text websocket) 61 | 62 | () 63 | else 64 | do! next() 65 | } 66 | 67 | let juse (middlware : HttpContext -> (unit -> Job) -> Job) (app:IApplicationBuilder) = 68 | app.Use( 69 | Func<_,Func<_>,_>( 70 | fun env next -> 71 | middlware env (next.Invoke >> Job.awaitUnitTask) 72 | |> Hopac.startAsTask :> Task 73 | )) 74 | let configureEchoServer (appBuilder : IApplicationBuilder) = 75 | appBuilder.UseWebSockets() 76 | |> juse (echoWebSocket) 77 | |> ignore 78 | 79 | () 80 | 81 | let getTestServer () = 82 | new TestServer( 83 | WebHostBuilder() 84 | .Configure(fun app -> configureEchoServer app)) 85 | 86 | 87 | let constructLocalUri port = 88 | sprintf "http://127.0.0.1:%d" port 89 | 90 | let getKestrelServer uri = job { 91 | let configBuilder = new ConfigurationBuilder() 92 | let configBuilder = configBuilder.AddInMemoryCollection() 93 | let config = configBuilder.Build() 94 | config.["server.urls"] <- uri 95 | let host = WebHostBuilder() 96 | .UseConfiguration(config) 97 | .UseKestrel() 98 | .Configure(fun app -> configureEchoServer app ) 99 | .Build() 100 | 101 | do! host.StartAsync() |> Job.awaitUnitTask 102 | return host 103 | } 104 | 105 | let getOpenClientWebSocket (testServer : TestServer) = job { 106 | let ws = testServer.CreateWebSocketClient() 107 | return! ws.ConnectAsync(testServer.BaseAddress, CancellationToken.None) 108 | // return ws 109 | } 110 | 111 | let getOpenWebSocket uri = job { 112 | let ws = new ClientWebSocket() 113 | do! ws.ConnectAsync(uri, CancellationToken.None) |> Job.awaitUnitTask 114 | return ws 115 | } 116 | 117 | // So we're able to tell the operating system to get a random free port by passing 0 118 | // and the system usually doesn't reuse a port until it has to 119 | // *pray* 120 | let getPort () = 121 | let listener = new Sockets.TcpListener(IPAddress.Loopback,0) 122 | listener.Start() 123 | let port = (listener.LocalEndpoint :?> IPEndPoint).Port 124 | listener.Stop() 125 | port 126 | 127 | let inline getServerAndWs () = job { 128 | let uri = getPort () |> constructLocalUri 129 | // printfn "starting up %A" uri 130 | let builder = UriBuilder(uri) 131 | builder.Scheme <- "ws" 132 | let! server = getKestrelServer uri 133 | let! clientWebSocket = builder.Uri |> getOpenWebSocket 134 | return server, clientWebSocket 135 | } 136 | 137 | [] 138 | let tests = 139 | testList "samples" [ 140 | yield 141 | testCaseJob "CT" <| job { 142 | use ct = new CancellationTokenSource(50) 143 | let! result = 144 | Alt.choose [ 145 | timeOutMillis 100 |> Alt.afterFun ^ fun _ -> "timeout" 146 | Alt.fromCT ct.Token |> Alt.afterFun ^ fun _ -> "cancelled" 147 | ] 148 | Expect.equal result "cancelled" "not cancelled" 149 | } 150 | 151 | yield! 152 | [1..10] 153 | |> Seq.map ^ fun index -> 154 | testCaseJob (sprintf "Echo Hello World - %d" index) <| job { 155 | let! (server, clientWebSocket) = getServerAndWs() 156 | use server = server 157 | use clientWebSocket = clientWebSocket 158 | let expected = genStr (2000 * index) 159 | do! clientWebSocket |> WebSocket.sendMessageAsUTF8 expected 160 | let! actual = clientWebSocket |> WebSocket.receiveMessageAsUTF8 161 | Expect.equal actual expected "did not echo" 162 | } 163 | 164 | yield 165 | testCaseJob "Many concurrent writes to websocket should throw exception" <| job { 166 | // To create thie exception we actually have to run against Kestrel and not TestHost 167 | // Go figure trying to get a timing exception to occur isn't always reliable 168 | // Job.catch returns empty exception sometimes so we'll keep trying until we get the exception we're looking for 169 | let rec inner (attempt : int) = job { 170 | if attempt = 1000 then 171 | skiptest "Too many attempts. Skipping" 172 | else 173 | 174 | let! servers = getServerAndWs() |> Job.catch 175 | match servers with 176 | | Choice1Of2 (server, clientWebSocket) -> 177 | use server = server 178 | use clientWebSocket = clientWebSocket 179 | let! result = 180 | [1..(Environment.ProcessorCount + 5)] 181 | |> Seq.map ^ fun _ -> 182 | clientWebSocket |> WebSocket.sendMessageAsUTF8 (genStr 1000) 183 | |> Job.conIgnore 184 | |> Job.catch 185 | 186 | 187 | match result with 188 | | Choice2Of2 e -> 189 | match e with 190 | | :? AggregateException as ae -> 191 | let exns = ae.Flatten().InnerExceptions 192 | // exns 193 | // |> Expect.exceptionExists "System.Net.WebSockets.WebSocketException" "The WebSocket is in an invalid state ('Aborted') for this operation. Valid states are: 'Open, CloseReceived'" 194 | try 195 | exns 196 | |> Expect.exceptionExists "System.InvalidOperationException" "There is already one outstanding 'SendAsync' call for this WebSocket instance. ReceiveAsync and SendAsync can be called simultaneously, but at most one outstanding operation for each of them is allowed at the same time." 197 | with _ -> do! inner(attempt + 1) 198 | | e -> do! inner (attempt + 1) 199 | | _ -> 200 | do! inner (attempt + 1) 201 | | Choice2Of2 e -> 202 | do! inner (attempt + 1) 203 | } 204 | do! inner 0 205 | } 206 | 207 | yield 208 | testCaseJob "Many concurrent writes to ThreadSafeWebSocket shouldn't throw exception" <| job { 209 | let! (server, clientWebSocket) = getServerAndWs() 210 | use server = server 211 | use clientWebSocket = clientWebSocket 212 | let! threadSafeWebSocket = ThreadSafeWebSocket.createFromWebSocket clientWebSocket 213 | 214 | let maxMessagesToSend = 5000 215 | 216 | let! receiveResult = 217 | [1..maxMessagesToSend] 218 | |> Seq.map ^ fun _ -> 219 | ThreadSafeWebSocket.receiveMessageAsUTF8 threadSafeWebSocket 220 | |> Job.conCollect 221 | |> Promise.start 222 | 223 | let expected = 224 | [1..maxMessagesToSend] 225 | |> Seq.map ^ fun _ -> (genStr 10000) 226 | |> Seq.toList 227 | expected 228 | |> Seq.iter (ThreadSafeWebSocket.sendMessageAsUTF8 threadSafeWebSocket >> start) 229 | 230 | let! receiveResult = receiveResult 231 | 232 | Expect.sequenceEqual (receiveResult |> Seq.sort) (expected |> Seq.sort) "Didn't echo properly" 233 | 234 | do! ThreadSafeWebSocket.close threadSafeWebSocket WebSocketCloseStatus.NormalClosure "End Test" 235 | } 236 | ] 237 | -------------------------------------------------------------------------------- /tests/Hopac.Websockets.Tests/paket.references: -------------------------------------------------------------------------------- 1 | Expecto 2 | FSharp.Core 3 | Microsoft.DotNet.Watcher.Tools 4 | dotnet-mono 5 | Microsoft.AspNetCore.TestHost 6 | Microsoft.AspNetCore.WebSockets 7 | Microsoft.AspNetCore.Server.Kestrel --------------------------------------------------------------------------------