├── .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 | [](https://travis-ci.org/TheAngryByrd/Hopac.Websockets) | [](https://ci.appveyor.com/project/TheAngryByrd/Hopac-Websockets)
17 | [](https://travis-ci.org/TheAngryByrd/Hopac.Websockets/builds) | [](https://ci.appveyor.com/project/TheAngryByrd/Hopac-Websockets)
18 |
19 |
20 | ## Nuget
21 |
22 | Package | Stable | Prerelease
23 | --- | --- | ---
24 | Hopac.Websockets | [](https://www.nuget.org/packages/Hopac.Websockets/) | [](https://www.nuget.org/packages/Hopac.Websockets/)
25 | Hopac.Websockets.AspNetCore | [](https://www.nuget.org/packages/Hopac.Websockets.AspNetCore/) | [](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
--------------------------------------------------------------------------------