table of contents · an introduction to application scaling scaling node.js applications the three...
TRANSCRIPT
TableofContentsNode.jsDesignPatternsCreditsAbouttheAuthorAcknowledgmentsAbouttheReviewerswww.PacktPub.comSupportfiles,eBooks,discountoffers,andmore
Whysubscribe?FreeaccessforPacktaccountholders
PrefaceWhatthisbookcoversWhatyouneedforthisbookWhothisbookisforConventionsReaderfeedbackCustomersupport
DownloadingtheexamplecodeErrataPiracyQuestions
1.Node.jsDesignFundamentalsTheNode.jsphilosophy
SmallcoreSmallmodulesSmallsurfaceareaSimplicityandpragmatism
ThereactorpatternI/OisslowBlockingI/ONon-blockingI/OEventdemultiplexingThereactorpatternThenon-blockingI/OengineofNode.js–libuvTherecipeforNode.js
ThecallbackpatternThecontinuation-passingstyleSynchronouscontinuation-passingstyleAsynchronouscontinuation-passingstyleNoncontinuation-passingstylecallbacks
Synchronousorasynchronous?AnunpredictablefunctionUnleashingZalgoUsingsynchronousAPIsDeferredexecution
Node.jscallbackconventionsCallbackscomelastErrorcomesfirstPropagatingerrorsUncaughtexceptions
ThemodulesystemanditspatternsTherevealingmodulepatternNode.jsmodulesexplainedAhomemademoduleloaderDefiningamoduleDefiningglobalsmodule.exportsvsexportsrequireissynchronousTheresolvingalgorithmThemodulecacheCycles
ModuledefinitionpatternsNamedexportsExportingafunctionExportingaconstructorExportinganinstanceModifyingothermodulesortheglobalscope
TheobserverpatternTheEventEmitterCreateanduseanEventEmitterPropagatingerrorsMakeanyobjectobservableSynchronousandasynchronouseventsEventEmittervsCallbacksCombinecallbacksandEventEmitter
Summary2.AsynchronousControlFlowPatternsThedifficultiesofasynchronousprogramming
CreatingasimplewebspiderThecallbackhell
UsingplainJavaScriptCallbackdisciplineApplyingthecallbackdiscipline
SequentialexecutionExecutingaknownsetoftasksinsequenceSequentialiterationWebspiderversion2SequentialcrawlingoflinksThepattern
ParallelexecutionWebspiderversion3ThepatternFixingraceconditionsinthepresenceofconcurrenttasks
LimitedparallelexecutionLimitingtheconcurrencyGloballylimitingtheconcurrencyQueuestotherescueWebspiderversion4
TheasynclibrarySequentialexecutionSequentialexecutionofaknownsetoftasksSequentialiteration
ParallelexecutionLimitedparallelexecution
PromisesWhatisapromise?Promises/A+implementationsPromisifyingaNode.jsstylefunctionSequentialexecutionSequentialiterationSequentialiteration–thepattern
ParallelexecutionLimitedparallelexecution
GeneratorsThebasicsAsimpleexampleGeneratorsasiteratorsPassingvaluesbacktoagenerator
AsynchronouscontrolflowwithgeneratorsGenerator-basedcontrolflowusingco
SequentialexecutionParallelexecutionLimitedparallelexecutionProducer-consumerpatternLimitingthedownloadtasksconcurrency
Comparison
Summary3.CodingwithStreamsDiscoveringtheimportanceofstreams
BufferingvsStreamingSpatialefficiencyGzippingusingabufferedAPIGzippingusingstreams
TimeefficiencyComposability
GettingstartedwithstreamsAnatomyofstreamsReadablestreamsReadingfromastreamThenon-flowingmodeTheflowingmode
ImplementingReadablestreamsWritablestreamsWritingtoastreamBack-pressureImplementingWritablestreams
DuplexstreamsTransformstreamsImplementingTransformstreams
ConnectingstreamsusingpipesUsefulpackagesforworkingwithstreamsreadable-streamthroughandfrom
AsynchronouscontrolflowwithstreamsSequentialexecutionUnorderedparallelexecutionImplementinganunorderedparallelstreamImplementingaURLstatusmonitoringapplication
UnorderedlimitedparallelexecutionOrderedparallelexecution
PipingpatternsCombiningstreamsImplementingacombinedstream
ForkingstreamsImplementingamultiplechecksumgenerator
MergingstreamsCreatingatarballfrommultipledirectories
MultiplexinganddemultiplexingBuildingaremotelogger
Clientside–MultiplexingServerside–demultiplexingRunningthemux/demuxapplication
MultiplexinganddemultiplexingobjectstreamsSummary
4.DesignPatternsFactory
AgenericinterfaceforcreatingobjectsAmechanismtoenforceencapsulationBuildingasimplecodeprofilerInthewild
ProxyTechniquesforimplementingproxiesObjectcompositionObjectaugmentation
AcomparisonofthedifferenttechniquesCreatingaloggingWritablestreamProxyintheecosystem–functionhooksandAOPInthewild
DecoratorTechniquesforimplementingdecoratorsCompositionObjectaugmentation
DecoratingaLevelUPdatabaseIntroducingLevelUPandLevelDBImplementingaLevelUPplugin
InthewildAdapter
UsingLevelUPthroughthefilesystemAPIInthewild
StrategyMulti-formatconfigurationobjectsInthewild
StateImplementingabasicfail-safesocket
TemplateAconfigurationmanagertemplateInthewild
MiddlewareMiddlewareinExpressMiddlewareasapatternCreatingamiddlewareframeworkfor ØMQTheMiddlewareManager
AmiddlewaretosupportJSONmessagesUsingtheØMQmiddlewareframeworkTheserverTheclient
CommandAflexiblepatternThetaskpatternAmorecomplexcommand
Summary5.WiringModulesModulesanddependencies
ThemostcommondependencyinNode.jsCohesionandCouplingStatefulmodulesTheSingletonpatterninNode.js
PatternsforwiringmodulesHardcodeddependencyBuildinganauthenticationserverusinghardcodeddependenciesThedbmoduleTheauthServicemoduleTheauthControllermoduleTheappmoduleRunningtheauthenticationserver
ProsandconsofhardcodeddependenciesDependencyinjectionRefactoringtheauthenticationservertousedependencyinjectionThedifferenttypesofdependencyinjectionProsandconsofdependencyinjection
ServicelocatorRefactoringtheauthenticationservertouseaservicelocatorProsandconsofaservicelocator
DependencyinjectioncontainerDeclaringasetofdependenciestoaDIcontainerRefactoringtheauthenticationservertouseaDIcontainerProsandconsofaDependencyInjectioncontainer
WiringpluginsPluginsaspackagesExtensionpointsPlugin-controlledvsApplication-controlledextensionImplementingalogoutpluginUsinghardcodeddependenciesExposingservicesusingaservicelocatorExposingservicesusingdependencyinjection
ExposingservicesusingadependencyinjectioncontainerSummary
6.RecipesRequiringasynchronouslyinitializedmodules
CanonicalsolutionsPreinitializationqueuesImplementingamodulethatinitializesasynchronouslyWrappingthemodulewithpreinitializationqueues
InthewildAsynchronousbatchingandcaching
ImplementingaserverwithnocachingorbatchingAsynchronousrequestbatchingBatchingrequestsinthetotalsaleswebserver
AsynchronousrequestcachingCachingrequestsinthetotalsaleswebserverNotesaboutimplementingcachingmechanisms
BatchingandcachingwithPromisesRunningCPU-boundtasks
SolvingthesubsetsumproblemInterleavingwithsetImmediateInterleavingthestepsofthesubsetsumalgorithmConsiderationsontheinterleavingpattern
UsingmultipleprocessesDelegatingthesubsetsumtasktootherprocessesImplementingaprocesspoolCommunicatingwithachildprocessCommunicatingwiththeparentprocess
ConsiderationsonthemultiprocesspatternSharingcodewiththebrowser
SharingmodulesUniversalModuleDefinitionCreatinganUMDmoduleConsiderationsontheUMDpattern
IntroducingBrowserifyExploringthemagicofBrowserifyTheadvantagesofusingBrowserify
Fundamentalsofcross-platformdevelopmentRuntimecodebranchingBuild-timecodebranchingDesignpatternsforcross-platformdevelopment
SharingbusinesslogicanddatavalidationusingBackboneModelsImplementingthesharedmodelsImplementingtheplatform-specificcode
UsingtheisomorphicmodelsRunningtheapplication
Summary7.ScalabilityandArchitecturalPatternsAnintroductiontoapplicationscaling
ScalingNode.jsapplicationsThethreedimensionsofscalability
CloningandloadbalancingTheclustermoduleNotesonthebehavioroftheclustermoduleBuildingasimpleHTTPserverScalingwiththeclustermoduleResiliencyandavailabilitywiththeclustermoduleZero-downtimerestart
DealingwithstatefulcommunicationsSharingthestateacrossmultipleinstancesStickyloadbalancing
ScalingwithareverseproxyLoadbalancingwithNginx
UsingaServiceRegistryImplementingadynamicloadbalancerwithhttp-proxyandseaport
Peer-to-peerloadbalancingImplementinganHTTPclientthatcanbalancerequestsacrossmultiple
serversDecomposingcomplexapplications
MonolithicarchitectureTheMicroservicearchitectureAnexampleoftheMicroservicearchitectureProsandconsofmicroservicesEveryserviceisexpendableReusabilityacrossplatformsandlanguagesAwaytoscaletheapplicationThechallengesofmicroservices
IntegrationpatternsinaMicroservicearchitectureTheAPIproxyAPIorchestrationIntegrationwithamessagebroker
Summary8.MessagingandIntegrationPatternsFundamentalsofamessagingsystem
One-wayandrequest/replypatternsMessagetypesAsynchronousmessagingandqueues
Peer-to-peerorbroker-basedmessagingPublish/subscribepattern
Buildingaminimalistreal-timechatapplicationImplementingtheserversideImplementingtheclientsideRunningandscalingthechatapplication
UsingRedisasamessagebrokerPeer-to-peerpublish/subscribewithØMQIntroducingØMQDesigningapeer-to-peerarchitectureforthechatserverUsingtheØMQPUB/SUBsockets
DurablesubscribersIntroducingAMQPDurablesubscriberswithAMQPandRabbitMQDesigningahistoryserviceforthechatapplicationImplementingareliablehistoryserviceusingAMQPIntegratingthechatapplicationwithAMQP
PipelinesandtaskdistributionpatternsTheØMQfan-out/fan-inpatternPUSH/PULLsocketsBuildingadistributedhashsumcrackerwithØMQImplementingtheventilatorImplementingtheworkerImplementingthesinkRunningtheapplication
PipelinesandcompetingconsumersinAMQPPoint-to-pointcommunicationsandcompetingconsumersImplementingthehashsumcrackerusingAMQPImplementingtheproducerImplementingtheworkerImplementingtheresultcollectorRunningtheapplication
Request/replypatternsCorrelationidentifierImplementingarequest/replyabstractionusingcorrelationidentifiersAbstractingtherequestAbstractingthereplyTryingthefullrequest/replycycle
ReturnaddressImplementingthereturnaddresspatterninAMQPImplementingtherequestabstractionImplementingthereplyabstractionImplementingtherequestorandthereplier
SummaryIndex
Node.jsDesignPatterns
Node.jsDesignPatternsCopyright©2014PacktPublishing
Allrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem,ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthepublisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews.
Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyoftheinformationpresented.However,theinformationcontainedinthisbookissoldwithoutwarranty,eitherexpressorimplied.Neithertheauthor,norPacktPublishing,anditsdealersanddistributorswillbeheldliableforanydamagescausedorallegedtobecauseddirectlyorindirectlybythisbook.
PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthecompaniesandproductsmentionedinthisbookbytheappropriateuseofcapitals.However,PacktPublishingcannotguaranteetheaccuracyofthisinformation.
Firstpublished:December2014
Productionreference:1231214
PublishedbyPacktPublishingLtd.
LiveryPlace
35LiveryStreet
BirminghamB32PB,UK.
ISBN978-1-78328-731-4
www.packtpub.com
CoverimagebyArtieNg(<[email protected]>)
CreditsAuthor
MarioCasciaro
Reviewers
AfshinMehrabani
JoelPurra
AlanShaw
CommissioningEditor
JulianUrsell
AcquisitionEditor
RebeccaYoué
ContentDevelopmentEditor
SriramNeelakantan
TechnicalEditor
MenzaMathew
CopyEditors
ShambhaviPai
RashmiSawant
ProjectCoordinator
AboliAmbardekar
Proofreaders
StephenCopestake
AmeeshaGreen
SteveMaguire
Indexers
HemanginiBari
MariammalChettiyar
RekhaNair
TejalSoni
Graphics
ValentinaD'silva
DishaHaria
AbhinashSahu
ProductionCoordinator
NiteshThakur
CoverWork
NiteshThakur
AbouttheAuthorMarioCasciaroisasoftwareengineerandtechnicalleadwithapassionforopensource.HebeganprogrammingwithaCommodore64whenhewas12,andgrewupwithPascalandVisualBasic.Hisprogrammingskillsevolvedbyexperimentingwithx86assemblylanguage,C,C++,PHP,andJava.HisrelentlessworkonsideprojectsledhimtodiscoverJavaScriptandNode.js,whichquicklybecamehisnewpassion.
Inhisprofessionalcareer,heworkedwithIBMforseveralyears—firstinRomeandthenintheDublinSoftwareLab.AtIBM,MarioworkedonproductsforbrandssuchasTivoli,Cognos,andCollaborationSolutions,usingavarietyoftechnologiesfromCtoPHPandJava.HethenplungedintotheadventurousworldofstartupstoworkfulltimeonNode.jsprojects.Heendedupworkinginalighthouse,atD4HTechnologies,whereheledthedevelopmentofareal-timeplatformtomanageemergencyoperations.
AcknowledgmentsThisbookistheresultofanamazingamountofwork,knowledge,andperseverancefrommanypeople.AbigthanksgoestotheentireteamatPacktwhomadethisbookareality,fromtheeditorstotheprojectcoordinator;inparticular,IwouldliketothankRebeccaYouéandSriramNeelakantanfortheirguidanceandpatienceduringthetoughestpartsofthewritingprocess.KudostoAlanShaw,JoelPurra,andAfshinMehrabaniwhodedicatedtheirtimeandexpertisetoreviewingthetechnicalcontentofthebook;everycommentandadvicewasreallyinvaluableinbringingthisworkuptoproductionquality.ThisbookwouldnotexistwithouttheeffortsofsomanypeoplewhomadeNode.jsareality—fromthebigplayers,whocontinuouslyinspiredus,tothecontributorofthesmallestmodule.
Inthesemonths,Ialsolearnedthatabookisonlypossiblewiththesupportandencouragementofallthepeoplearoundyou.Mygratitudegoestoallmyfriendswhoheardthephrase"TodayIcan't,Ihavetoworkonthebook"toomanytimes;thankstoChristopheGuillou,ZbigniewMrowinski,RyanGallagher,NataliaLopez,RuizhiWang,andDavideLionelloforstilltalkingtome.ThankstotheD4Hcrew,fortheirinspirationandunderstanding,andforgivingmethechancetoworkonafirst-classproduct.
ThankstoallthefriendsbackinItaly,tothelegendarycompanyofTavernaandCentrale,totheladsofLidoMariniforalwaysgivingmeagreattime,laughingandhavingfun.I'msorryfornotbeingpresentinthepastfewmonths.
ThankstomyMomandDad,andtomybrotherandsister,fortheirunconditionalloveandsupport.
Atlast,youhavetoknowthatthereisanotherpersonwhowrotethisbookalongwithme,that'sMiriam,mygirlfriend,whowalkedthroughoutthislongjourneywithmeandsupportedmenightandday,regardlessofhowdifficultitwas.There'snothingmoreonecouldwishfor.Isendallmyloveandgratitudetoher.Manyadventuresawaitus.
AbouttheReviewersAfshinMehrabaniisanopensourceprogrammer.Heisstudyingtobeacomputersoftwareengineer.Hestartedprogrammingandwebdevelopmentwhenhewas12yearsold,andstartedwithPHPaswell.Later,hejoinedtheIranTechnicalandVocationalTrainingOrganization.Hesecuredthefirstplaceandreceivedagoldmedalinacompetitionthatwasconductedacrosstheentirecountryintheareaofwebdevelopment.HebecameamemberofIran'sNationalElitesFoundationafterproducingavarietyofnewprogrammingideas.
HewasasoftwareengineeratTehranStockExchangeandisnowtheheadofthewebdevelopmentteaminYaraInternational.HecofoundedtheUsablicateaminearly2012todevelopandproduceusableapplications.HeistheauthorofIntroJs,WideArea,flood.js,andotheropensourceprojects.HehascontributedtoSocket.IO,Engine.IO,andotheropensourceprojects.Heisalsointerestedincreatingandcontributingtoopensourceapplications,writingprogrammingarticles,andchallenginghimselfwithnewprogrammingtechnologies.
HehaswrittendifferentarticlesonJavaScript,Node.js,HTML5,andMongoDB,whichhavebeenpublishedondifferentacademicwebsites.Afshinhas5yearsofexperienceinPHP,Python,C#,JavaScript,HTML5,andNode.jsinmanyfinancialandstock-tradingprojects.
JoelPurrastartedtoyingaroundwithcomputerssometimebeforehisteens,seeingthemasanotherkindofavideo-gamingdevice.Itwasnotlongbeforehetookapart(sometimesbrokeandsubsequentlyfixed)anycomputerhecameacross,inbetweenplayingthelatestgamesonthem.Itwasgamingthatledhimtodiscoverprogramminginhisearlyteens,whenmodifyingaLunarLandergametriggeredaninterestincreatingdigitaltools.SoonaftergettinganInternetconnectionathome,hedevelopedhisfirste-commercewebsite,andthushisbusinessstarted;itlaunchedhiscareeratanearlyage.
Attheageof17,Joelstartedstudyingcomputerprogrammingandanenergy/scienceprogramatanuclearpowerplant'sschool.Aftergraduation,hestudiedtobecomeaSecondLieutenantTelecommunicationsSpecialistintheSwedishArmy,beforemovingontostudyforhisMasterofSciencedegreeinInformationTechnologyandEngineeringatLinköpingUniversity.
Hehasbeeninvolvedinstartupsandothercompanies—bothsuccessfulandunsuccessful—since1998,andhasbeenaconsultantsince2007.Born,raised,andeducatedinSweden,Joelalsoenjoystheflexiblelifestyleofafreelancedeveloper,havingtraveledthroughfivecontinentswithhisbackpackandlivedabroadforseveral
years.Alearnerconstantlylookingforchallenges,oneofhisgoalsistobuildandevolvesoftwareforbroadpublicuse.
Youcanvisithiswebsiteathttp://joelpurra.com/.
I'dliketothanktheopensourcecommunityforgivingmethebuildingblocksnecessarytocomposebothsmallandlargesoftwaresystems,evenasafreelanceconsultant.Nanosgigantumhumerisinsidentes.Remembertocommitearly,commitoften!
AlanShawdescribeshimselfasawebdeveloperwhodiscoversthelimitsofthepossiblebyventuringalittlewaypastthemintotheimpossible.AlanhasbuiltandstyledtheWebeverydaysincegraduatingfromtheUniversityofBathwithadegreeincomputerscience.HeisanadvocateoffunctionalprogrammingandhasworkedwithJavaScriptforaslongashecanremember.
AlanandOliEvansownandrunTABLEFLIP,awebdevelopmentcompanythatfocusesonNode.js,goodclientrelationships,andgivingbacktothecommunitythroughopensourceprojects.
Inhissparetime,Alanhacksonnpmmodules,browserifytransforms,andgruntplugins.HebuildsandmaintainsDavid(https://david-dm.org),co-organizesthemeetupsforNodebotsofLondonandMeteorLondon,hacksonhardware,pilotsnanocoptersintowalls,andisacofounderoftheJavaScriptAdventureClub.
www.PacktPub.com
Supportfiles,eBooks,discountoffers,andmoreForsupportfilesanddownloadsrelatedtoyourbook,pleasevisitwww.PacktPub.com.
DidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFandePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandasaprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwithusat<[email protected]>formoredetails.
Atwww.PacktPub.com,youcanalsoreadacollectionoffreetechnicalarticles,signupforarangeoffreenewslettersandreceiveexclusivediscountsandoffersonPacktbooksandeBooks.
https://www2.packtpub.com/books/subscription/packtlib
DoyouneedinstantsolutionstoyourITquestions?PacktLibisPackt'sonlinedigitalbooklibrary.Here,youcansearch,access,andreadPackt'sentirelibraryofbooks.
Whysubscribe?FullysearchableacrosseverybookpublishedbyPacktCopyandpaste,print,andbookmarkcontentOndemandandaccessibleviaawebbrowser
FreeaccessforPacktaccountholdersIfyouhaveanaccountwithPacktatwww.PacktPub.com,youcanusethistoaccessPacktLibtodayandview9entirelyfreebooks.Simplyuseyourlogincredentialsfor
immediateaccess.
PrefaceNode.jsisconsideredbymanyasagame-changer—thebiggestshiftofthedecadeinwebdevelopment.Itislovednotjustforitstechnicalcapabilities,butalsoforthechangeofparadigmthatitintroducedinwebdevelopment.
First,Node.jsapplicationsarewritteninJavaScript,thelanguageoftheweb,theonlyprogramminglanguagesupportednativelybyamajorityofwebbrowsers.Thisaspectonlyenablesscenariossuchassingle-languageapplicationstacksandsharingofcodebetweentheserverandtheclient.Node.jsitselfiscontributingtotheriseandevolutionoftheJavaScriptlanguage.PeoplerealizethatusingJavaScriptontheserverisnotasbadasitisinthebrowser,andtheywillsoonstarttoloveitforitspragmatismandforitshybridnature,halfwaybetweenobject-orientedandfunctionalprogramming.
Thesecondrevolutionizingfactorisitssingle-threaded,asynchronousarchitecture.Besidesobviousadvantagesfromaperformanceandscalabilitypointofview,thischaracteristicchangedthewaydevelopersapproachconcurrencyandparallelism.Mutexesarereplacedbyqueues,threadsbycallbacksandevents,andsynchronizationbycausality.
ThelastandmostimportantaspectofNode.jsliesinitsecosystem:thenpmpackagemanager,itsconstantlygrowingdatabaseofmodules,itsenthusiasticandhelpfulcommunity,andmostimportantly,itsveryownculturebasedonsimplicity,pragmatism,andextrememodularity.
However,becauseofthesepeculiarities,Node.jsdevelopmentgivesyouaverydifferentfeelcomparedtotheotherserver-sideplatforms,andanydevelopernewtothisparadigmwilloftenfeelunsureabouthowtotackleeventhemostcommondesignandcodingproblemeffectively.Commonquestionsinclude:"HowdoIorganizemycode?","What'sthebestwaytodesignthis?","HowcanImakemyapplicationmoremodular?","HowdoIhandleasetofasynchronouscallseffectively?","HowcanImakesurethatmyapplicationwillnotcollapsewhileitgrows?",ormoresimply"What'stherightwayofdoingthis?"Fortunately,Node.jshasbecomeamature-enoughplatformandmostofthesequestionscannowbeeasilyansweredwithadesignpattern,aprovencodingtechnique,orarecommendedpractice.Theaimofthisbookistoguideyouthroughthisemergingworldofpatterns,techniques,andpractices,showingyouwhattheprovensolutionstothecommonproblemsareandteachingyouhowtousethemasthestartingpointtobuildingthesolutiontoyourparticularproblem.
Byreadingthisbook,youwilllearnthefollowing:
The"Nodeway".HowtousetherightpointofviewwhenapproachingaNode.jsdesignproblem.Youwilllearn,forexample,howdifferenttraditionaldesignpatternslookinNode.js,orhowtodesignmodulesthatdoonlyonething.AsetofpatternstosolvecommonNode.jsdesignandcodingproblems.Youwillbepresentedwitha"Swissarmyknife"ofpatterns,ready-to-useinordertoefficientlysolveyoureverydaydevelopmentanddesignproblems.HowtowritemodularandefficientNode.jsapplications.Youwillgainanunderstandingofthebasicbuildingblocksandprinciplesofwritinglargeandwell-organizedNode.jsapplicationsandyouwillbeabletoapplytheseprinciplestonovelproblemsthatdon'tfallwithinthescopeofexistingpatterns.
Throughoutthebook,youwillbepresentedwithseveralreal-lifelibrariesandtechnologies,suchasLevelDb,Redis,RabbitMQ,ZMQ,Express,andmanyothers.Theywillbeusedtodemonstrateapatternortechnique,andbesidesmakingtheexamplemoreuseful,thesewillalsogiveyougreatexposuretotheNode.jsecosystemanditssetofsolutions.
WhetheryouuseorplantouseNode.jsforyourwork,yoursideproject,orforanopensourceproject,recognizingandusingwell-knownpatternsandtechniqueswillallowyoutouseacommonlanguagewhensharingyourcodeanddesign,andontopofthat,itwillhelpyougetabetterunderstandingaboutthefutureofNode.jsandhowtomakeyourowncontributionsapartofit.
WhatthisbookcoversChapter1,Node.jsDesignFundamentals,servesasanintroductiontotheworldofNode.jsapplicationdesignbyshowingthepatternsatthecoreoftheplatformitself.Itcoversthereactorpattern,thecallbackpattern,themodulepattern,andtheobserverpattern.
Chapter2,AsynchronousControlFlowPatterns,introducesasetofpatternsandtechniquesforefficientlyhandlingasynchronouscontrolflowinNode.js.Thischapterteachesyouhowtomitigatethe"callbackhell"problemusingplainJavaScript,theasynclibrary,Promises,andGenerators.
Chapter3,CodingwithStreams,divesdeeplyintooneofthemostimportantpatternsinNode.js:Streams.Itshowsyouhowtoprocessdatawithtransformstreamsandhowtocombinethemintodifferentlayouts.
Chapter4,DesignPatterns,dealswithacontroversialtopic:traditionaldesignpatternsinNode.js.ItcoversthemostpopularconventionaldesignpatternsandshowsyouhowunconventionaltheymightlookinNode.js.
Chapter5,WiringModules,analyzesthedifferentsolutionsforlinkingthemodulesofanapplicationtogether.Inthischapter,youwilllearndesignpatternssuchasDependencyInjectionandServicelocator.
Chapter6,Recipes,takesaproblem-solutionapproachtoshowyouhowsomecommoncodinganddesignchallengescanbesolvedwithready-to-usesolutions.
Chapter7,ScalabilityandArchitecturalPatterns,teachesyouthebasictechniquesandpatternsforscalingaNode.jsapplication.
Chapter8,MessagingandIntegrationPatterns,presentsthemostimportantmessagingpatterns,teachingyouhowtobuildandintegratecomplexdistributedsystemsusingZMQandAMQP.
WhatyouneedforthisbookToexperimentwiththecode,youwillneedaworkinginstallationofNode.jsversion0.10(orgreater)andnpm.SomeexampleswillrequireNode.js0.11orgreater.Youwillalsoneedtobefamiliarwiththecommandprompt,knowhowtoinstallannpmpackage,andknowhowtorunNode.jsapplications.Youwillalsoneedatexteditortoworkwiththecodeandawebbrowser.
WhothisbookisforThisbookisfordeveloperswhohavealreadyhadinitialcontactwithNode.jsandnowwanttogetthemostoutofitintermsofproductivity,designquality,andscalability.Youareonlyrequiredtohavesomepriorexposuretothetechnologythroughsomebasicexamples,sincethisbookwillcoversomebasicconceptsaswell.DeveloperswithintermediateexperienceinNode.jswillalsofindthetechniquespresentedinthisbookbeneficial.
Somebackgroundinsoftwaredesigntheoryisalsoanadvantagetounderstandsomeoftheconceptspresented.
Thisbookassumesthatyouhaveaworkingknowledgeofwebapplicationdevelopment,JavaScript,webservices,databases,anddatastructures.
ConventionsInthisbook,youwillfindanumberofstylesoftextthatdistinguishbetweendifferentkindsofinformation.Herearesomeexamplesofthesestyles,andanexplanationoftheirmeaning.
Codewordsintextareshownasfollows:"Wecanincludeothercontextsthroughtheuseoftheincludedirective."
Ablockofcodeissetasfollows:
varzmq=require('zmq')
varsink=zmq.socket('pull');
sink.bindSync("tcp://*:5001");
sink.on('message',function(buffer){
console.log('Messagefromworker:',buffer.toString());
});
Whenwewishtodrawyourattentiontoaparticularpartofacodeblock,therelevantlinesoritemsaresetinbold:
functionproduce(){
[...]
variationsStream(alphabet,maxLength)
.on('data',function(combination){
[...]
varmsg={searchHash:searchHash,variations:batch};
channel.sendToQueue('jobs_queue',
newBuffer(JSON.stringify(msg)));
[...]
}
})
[...]
}
Anycommand-lineinputoroutputiswrittenasfollows:
nodereplier
noderequestor
Newtermsandimportantwordsareshowninbold.Wordsthatyouseeonthescreen,inmenusordialogboxesforexample,appearinthetextlikethis:"Toexplaintheproblem,wewillcreatealittlewebspider,acommand-lineapplicationthattakesinawebURLastheinputanddownloadsitscontentslocallyintoafile."
NoteWarningsorimportantnotesappearinaboxlikethis.
TipTipsandtricksappearlikethis.
ReaderfeedbackFeedbackfromourreadersisalwayswelcome.Letusknowwhatyouthinkaboutthisbook—whatyoulikedormayhavedisliked.Readerfeedbackisimportantforustodeveloptitlesthatyoureallygetthemostoutof.
Tosendusgeneralfeedback,simplysendane-mailto<[email protected]>,andmentionthebooktitlethroughthesubjectofyourmessage.
Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingorcontributingtoabook,seeourauthorguideonwww.packtpub.com/authors.
CustomersupportNowthatyouaretheproudownerofaPacktbook,wehaveanumberofthingstohelpyoutogetthemostfromyourpurchase.
DownloadingtheexamplecodeYoucandownloadtheexamplecodefilesforallPacktbooksyouhavepurchasedfromyouraccountathttp://www.packtpub.com.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.
ErrataAlthoughwehavetakeneverycaretoensuretheaccuracyofourcontent,mistakesdohappen.Ifyoufindamistakeinoneofourbooks—maybeamistakeinthetextorthecode—wewouldbegratefulifyouwouldreportthistous.Bydoingso,youcansaveotherreadersfromfrustrationandhelpusimprovesubsequentversionsofthisbook.Ifyoufindanyerrata,pleasereportthembyvisitinghttp://www.packtpub.com/support,selectingyourbook,clickingontheerratasubmissionformlink,andenteringthedetailsofyourerrata.Onceyourerrataareverified,yoursubmissionwillbeacceptedandtheerratawillbeuploadedtoourwebsite,oraddedtoanylistofexistingerrata,undertheErratasectionofthattitle.
PiracyPiracyofcopyrightmaterialontheInternetisanongoingproblemacrossallmedia.AtPackt,wetaketheprotectionofourcopyrightandlicensesveryseriously.Ifyoucomeacrossanyillegalcopiesofourworks,inanyform,ontheInternet,pleaseprovideuswiththelocationaddressorwebsitenameimmediatelysothatwecanpursuearemedy.
Pleasecontactusat<[email protected]>withalinktothesuspectedpiratedmaterial.
Weappreciateyourhelpinprotectingourauthors,andourabilitytobringyouvaluablecontent.
Questions
Youcancontactusat<[email protected]>ifyouarehavingaproblemwithanyaspectofthebook,andwewilldoourbesttoaddressit.
Chapter1.Node.jsDesignFundamentalsSomeprinciplesanddesignpatternsliterallydefinetheNode.jsplatformanditsecosystem;themostpeculiaronesareprobablyitsasynchronousnatureanditsprogrammingstylethatmakesheavyuseofcallbacks.However,thereareotherfundamentalcomponentsthatcharacterizetheplatform;forexample,itsmodulesystem,whichallowsmultipleversionsofthesamedependencytocoexistinanapplication,andtheobserverpattern,implementedbytheEventEmitterclass,whichperfectlycomplementscallbackswhendealingwithasynchronouscode.It'sthereforeimportantthatwefirstdiveintothesefundamentalprinciplesandpatterns,notonlyforwritingcorrectcode,butalsotobeabletotakeeffectivedesigndecisionswhenitcomestosolvingbiggerandmorecomplexproblems.
AnotheraspectthatcharacterizesNode.jsisitsphilosophy.ApproachingNode.jsisinfactwaymorethansimplylearninganewtechnology;it'salsoembracingacultureandacommunity.Wewillseehowthisgreatlyinfluencesthewaywedesignourapplicationsandcomponents,andthewaytheyinteractwiththosecreatedbythecommunity.
Inthischapter,wewilllearnthefollowingtopics:
TheNode.jsphilosophy,the"Nodeway"Thereactorpattern:themechanismattheheartoftheNode.jsasynchronousarchitectureTheNode.jscallbackpatternanditssetofconventionsThemodulesystemanditspatterns:thefundamentalmechanismsfororganizingcodeinNode.jsTheobserverpatternanditsNode.jsincarnation:theEventEmitterclass
TheNode.jsphilosophyEveryplatformhasitsownphilosophy—asetofprinciplesandguidelinesthataregenerallyacceptedbythecommunity,anideologyofdoingthingsthatinfluencestheevolutionofaplatform,andhowapplicationsaredevelopedanddesigned.Someoftheseprinciplesarisefromthetechnologyitself,someofthemareenabledbyitsecosystem,somearejusttrendsinthecommunity,andothersareevolutionsofdifferentideologies.InNode.js,someoftheseprinciplescomedirectlyfromitscreator,RyanDahl,fromallthepeoplewhocontributedtothecore,fromcharismaticfiguresinthecommunity,andsomeoftheprinciplesareinheritedfromtheJavaScript
cultureorareinfluencedbytheUnixphilosophy.
Noneoftheserulesareimposedandtheyshouldalwaysbeappliedwithcommonsense;however,theycanprovetobetremendouslyusefulwhenwearelookingforasourceofinspirationwhiledesigningourprograms.
NoteYoucanfindanextensivelistofsoftwaredevelopmentphilosophiesinWikipediaathttp://en.wikipedia.org/wiki/List_of_software_development_philosophies.
SmallcoreTheNode.jscoreitselfhasitsfoundationsbuiltonafewprinciples;oneoftheseis,havingthesmallestsetoffunctionality,leavingtheresttotheso-calleduserland(oruserspace),theecosystemofmoduleslivingoutsidethecore.ThisprinciplehasanenormousimpactontheNode.jsculture,asitgivesfreedomtothecommunitytoexperimentanditeratefastonabroadersetofsolutionswithinthescopeoftheuserlandmodules,insteadofbeingimposedwithoneslowlyevolvingsolutionthatisbuiltintothemoretightlycontrolledandstablecore.Keepingthecoresetoffunctionalitytothebareminimumthen,notonlybecomesconvenientintermsofmaintainability,butalsointermsofthepositiveculturalimpactthatitbringsontheevolutionoftheentireecosystem.
SmallmodulesNode.jsusestheconceptofmoduleasafundamentalmeantostructurethecodeofaprogram.Itisthebrickforcreatingapplicationsandreusablelibrariescalledpackages(apackageisalsofrequentlyreferredtoasjustmodule;since,usuallyithasonesinglemoduleasanentrypoint).InNode.js,oneofthemostevangelizedprinciplesistodesignsmallmodules,notonlyintermsofcodesize,butmostimportantlyintermsofscope.
ThisprinciplehasitsrootsintheUnixphilosophy,particularlyintwoofitsprecepts,whichareasfollows:
"Smallisbeautiful.""Makeeachprogramdoonethingwell."
Node.jsbroughttheseconceptstoawholenewlevel.Alongwiththehelpofnpm,theofficialpackagemanager,Node.jshelpssolvingthedependencyhellproblemby
makingsurethateachinstalledpackagewillhaveitsownseparatesetofdependencies,thusenablingaprogramtodependonalotofpackageswithoutincurringinconflicts.TheNodeway,infact,involvesextremelevelsofreusability,wherebyapplicationsarecomposedofahighnumberofsmall,well-focuseddependencies.Whilethiscanbeconsideredunpracticaloreventotallyunfeasibleinotherplatforms,inNode.jsthispracticeisencouraged.Asaconsequence,itisnotraretofindnpmpackagescontaininglessthan100linesofcodeorexposingonlyonesinglefunction.
Besidestheclearadvantageintermsofreusability,asmallmoduleisalsoconsideredtobethefollowing:
EasiertounderstandanduseSimplertotestandmaintainPerfecttosharewiththebrowser
Havingsmallerandmorefocusedmodulesempowerseveryonetoshareorreuseeventhesmallestpieceofcode;it'stheDon'tRepeatYourself(DRY)principleappliedatawholenewlevel.
SmallsurfaceareaInadditiontobeingsmallinsizeandscope,Node.jsmodulesusuallyalsohavethecharacteristicofexposingonlyaminimalsetoffunctionality.ThemainadvantagehereisanincreasedusabilityoftheAPI,whichmeansthattheAPIbecomesclearertouseandislessexposedtoerroneoususage.Mostofthetime,infact,theuserofacomponentisinterestedonlyinaverylimitedandfocusedsetoffeatures,withouttheneedtoextenditsfunctionalityortapintomoreadvancedaspects.
InNode.js,averycommonpatternfordefiningmodulesistoexposeonlyonepieceoffunctionality,suchasafunctionoraconstructor,whilelettingmoreadvancedaspectsorsecondaryfeaturesbecomepropertiesoftheexportedfunctionorconstructor.Thishelpstheusertoidentifywhatisimportantandwhatissecondary.Itisnotraretofindmodulesthatexposeonlyonefunctionandnothingelse,forthesimplefactthatitprovidesasingle,unmistakablyclearentrypoint.
AnothercharacteristicofmanyNode.jsmodulesisthefactthattheyarecreatedtobeusedratherthanextended.Lockingdowntheinternalsofamodulebyforbiddinganypossibilityofanextensionmightsoundinflexible,butitactuallyhastheadvantageofreducingtheusecases,simplifyingitsimplementation,facilitatingitsmaintenance,andincreasingitsusability.
Simplicityandpragmatism
HaveyoueverheardoftheKeepItSimple,Stupid(KISS)principle?Orthefamousquote:
"Simplicityistheultimatesophistication."
--LeonardodaVinci
RichardP.Gabriel,aprominentcomputerscientistcoinedthetermworseisbettertodescribethemodel,wherebylessandsimplerfunctionalityisagooddesignchoiceforsoftware.Inhisessay,Theriseofworseisbetter,hesays:
"Thedesignmustbesimple,bothinimplementationandinterface.Itismoreimportantfortheimplementationtobesimplethantheinterface.Simplicityisthe
mostimportantconsiderationinadesign."
Designingasimple,asopposedtoaperfect,feature-fullsoftware,isagoodpracticeforseveralreasons:ittakeslessefforttoimplement,allowsfastershippingwithlessresources,iseasiertoadapt,andiseasiertomaintainandunderstand.Thesefactorsfosterthecommunitycontributionsandallowthesoftwareitselftogrowandimprove.
InNode.js,thisprincipleisalsoenabledbyJavaScript,whichisaverypragmaticlanguage.It'snotrare,infact,toseesimplefunctions,closures,andobjectliteralsreplacingcomplexclasshierarchies.Pureobject-orienteddesignsoftentrytoreplicatetherealworldusingthemathematicaltermsofacomputersystemwithoutconsideringtheimperfectionandthecomplexityoftherealworlditself.Thetruthisthatoursoftwareisalwaysanapproximationoftherealityandwewouldprobablyhavemoresuccessintryingtogetsomethingworkingsoonerandwithreasonablecomplexity,insteadoftryingtocreateanear-perfectsoftwarewithahugeeffortandtonsofcodetomaintain.
Throughoutthisbook,wewillseethisprincipleinactionmanytimes.Forexample,aconsiderablenumberoftraditionaldesignpatterns,suchasSingletonorDecoratorcanhaveatrivial,evenifsometimesnotfoolproofimplementationandwewillseehowanuncomplicated,practicalapproachmostofthetimeispreferredtoapure,flawlessdesign.
ThereactorpatternInthissection,wewillanalyzethereactorpattern,whichistheheartoftheNode.jsasynchronousnature.Wewillgothroughthemainconceptsbehindthepattern,suchasthesingle-threadedarchitectureandthenon-blockingI/O,andwewillseehowthiscreatesthefoundationfortheentireNode.jsplatform.
I/OisslowI/Oisdefinitelytheslowestamongthefundamentaloperationsofacomputer.AccessingtheRAMisintheorderofnanoseconds(10e-9seconds),whileaccessingdataonthediskorthenetworkisintheorderofmilliseconds(10e-3seconds).Forthebandwidth,itisthesamestory;RAMhasatransferrateconsistentlyintheorderofGB/s,whilediskandnetworkvariesfromMB/sto,optimistically,GB/s.I/OisusuallynotexpensiveintermsofCPU,butitaddsadelaybetweenthemomenttherequestissentandthemomenttheoperationcompletes.Ontopofthat,wealsohavetoconsiderthehumanfactor;often,theinputofanapplicationcomesfromarealperson,forexample,theclickofabuttonoramessagesentinareal-timechatapplication,sothespeedandfrequencyofI/Odon'tdependonlyontechnicalaspects,andtheycanbemanyordersofmagnitudeslowerthanthediskornetwork.
BlockingI/OIntraditionalblockingI/Oprogramming,thefunctioncallcorrespondingtoanI/Orequestwillblocktheexecutionofthethreaduntiltheoperationcompletes.Thiscangofromafewmilliseconds,incaseofadiskaccess,tominutesorevenmore,incasethedataisgeneratedfromuseractions,suchaspressingakey.Thefollowingpseudocodeshowsatypicalblockingreadperformedagainstasocket:
//blocksthethreaduntilthedataisavailable
data=socket.read();
//dataisavailable
print(data);
ItistrivialtonoticethatawebserverthatisimplementedusingblockingI/Owillnotbeabletohandlemultipleconnectionsinthesamethread;eachI/Ooperationonasocketwillblocktheprocessingofanyotherconnection.Forthisreason,thetraditionalapproachtohandleconcurrencyinwebserversistokickoffathreadoraprocess(ortoreuseonetakenfromapool)foreachconcurrentconnectionthatneedstobehandled.Thisway,whenathreadblocksforanI/Ooperationitwillnotimpacttheavailabilityoftheotherrequests,becausetheyarehandledinseparate
threads.
Thefollowingimageillustratesthisscenario:
Theprecedingimagelaysemphasisontheamountoftimeeachthreadisidle,waitingfornewdatatobereceivedfromtheassociatedconnection.Now,ifwealsoconsiderthatanytypeofI/Ocanpossiblyblockarequest,forexample,whileinteractingwithdatabasesorwiththefilesystem,wesoonrealizehowmanytimesathreadhastoblockinordertowaitfortheresultofanI/Ooperation.Unfortunately,athreadisnotcheapintermsofsystemresources,itconsumesmemoryandcausescontextswitches,sohavingalongrunningthreadforeachconnectionandnotusingitformostofthetime,isnotthebestcompromiseintermsofefficiency.
Non-blockingI/OInadditiontoblockingI/O,mostmodernoperatingsystemssupportanothermechanismtoaccessresources,callednon-blockingI/O.Inthisoperatingmode,thesystemcallalwaysreturnsimmediatelywithoutwaitingforthedatatobereadorwritten.Ifnoresultsareavailableatthemomentofthecall,thefunctionwillsimplyreturnapredefinedconstant,indicatingthatthereisnodataavailabletoreturnatthatmoment.
Forexample,inUnixoperatingsystems,thefcntl()functionisusedtomanipulateanexistingfiledescriptortochangeitsoperatingmodetonon-blocking(withtheO_NONBLOCKflag).Oncetheresourceisinnon-blockingmode,anyreadoperationwillfailwithareturncode,EAGAIN,incasetheresourcedoesn'thaveanydatareadytoberead.
Themostbasicpatternforaccessingthiskindofnon-blockingI/Oistoactivelypolltheresourcewithinaloopuntilsomeactualdataisreturned;thisiscalledbusy-waiting.Thefollowingpseudocodeshowsyouhowit'spossibletoreadfrommultiple
resourcesusingnon-blockingI/Oandapollingloop:
resources=[socketA,socketB,pipeA];
while(!resources.isEmpty()){
for(i=0;i<resources.length;i++){
resource=resources[i];
//trytoread
vardata=resource.read();
if(data===NO_DATA_AVAILABLE)
//thereisnodatatoreadatthemoment
continue;
if(data===RESOURCE_CLOSED)
//theresourcewasclosed,removeitfromthelist
resources.remove(i);
else
//somedatawasreceived,processit
consumeData(data);
}
}
Youcanseethat,withthissimpletechnique,itisalreadypossibletohandledifferentresourcesinthesamethread,butit'sstillnotefficient.Infact,intheprecedingexample,theloopwillconsumepreciousCPUonlyforiteratingoverresourcesthatareunavailablemostofthetime.PollingalgorithmsusuallyresultinahugeamountofwastedCPUtime.
EventdemultiplexingBusy-waitingisdefinitelynotanidealtechniqueforprocessingnon-blockingresources,butluckily,mostmodernoperatingsystemsprovideanativemechanismtohandleconcurrent,non-blockingresourcesinanefficientway;thismechanismiscalledsynchronouseventdemultiplexeroreventnotificationinterface.ThiscomponentcollectsandqueuesI/Oeventsthatcomefromasetofwatchedresources,andblockuntilneweventsareavailabletoprocess.Thefollowingisthepseudocodeofanalgorithmthatusesagenericsynchronouseventdemultiplexertoreadfromtwodifferentresources:
socketA,pipeB;
watchedList.add(socketA,FOR_READ);//[1]
watchedList.add(pipeB,FOR_READ);
while(events=demultiplexer.watch(watchedList)){//[2]
//eventloop
foreach(eventinevents){//[3]
//Thisreadwillneverblockandwillalwaysreturndata
data=event.resource.read();
if(data===RESOURCE_CLOSED)
//theresourcewasclosed,removeitfromthewatched
list
demultiplexer.unwatch(event.resource);
else
//someactualdatawasreceived,processit
consumeData(data);
}
}
Thesearetheimportantstepsoftheprecedingpseudocode:
1. Theresourcesareaddedtoadatastructure,associatingeachoneofthemwithaspecificoperation,inourexamplearead.
2. Theeventnotifierissetupwiththegroupofresourcestobewatched.Thiscallissynchronousandblocksuntilanyofthewatchedresourcesisreadyforaread.Whenthisoccurs,theeventdemultiplexerreturnsfromthecallandanewsetofeventsisavailabletobeprocessed.
3. Eacheventreturnedbytheeventdemultiplexerisprocessed.Atthispoint,theresourceassociatedwitheacheventisguaranteedtobereadytoreadandtonotblockduringtheoperation.Whenalltheeventsareprocessed,theflowwillblockagainontheeventdemultiplexeruntilneweventsareagainavailabletobeprocessed.Thisiscalledtheeventloop.
It'sinterestingtoseethatwiththispattern,wecannowhandleseveralI/Ooperationsinsideasinglethread,withoutusingabusy-waitingtechnique.Thefollowingimageshowsushowawebserverwouldbeabletohandlemultipleconnectionsusingasynchronouseventdemultiplexerandasinglethread:
Thepreviousimagehelpsusunderstandhowconcurrencyworksinasingle-threadedapplicationusingasynchronouseventdemultiplexerandnon-blockingI/O.WecanseethatusingonlyonethreaddoesnotimpairourabilitytorunmultipleI/Oboundtasksconcurrently.Thetasksarespreadovertime,insteadofbeingspreadacrossmultiplethreads.Thishastheclearadvantageofminimizingthetotalidletimeofthethread,asclearlyshownintheimage.Thisisnottheonlyreasonforchoosingthismodel.Tohaveonlyasinglethread,infact,alsohasabeneficialimpactonthewayprogrammersapproachconcurrencyingeneral.Throughoutthebook,wewillsee
howtheabsenceofin-processraceconditionsandmultiplethreadstosynchronize,allowsustousemuchsimplerconcurrencystrategies.
Inthenextchapter,wewillhavetheopportunitytotalkmoreabouttheconcurrencymodelofNode.js.
ThereactorpatternWecannowintroducethereactorpattern,whichisaspecializationofthealgorithmpresentedintheprevioussection.Themainideabehinditistohaveahandler(whichinNode.jsisrepresentedbyacallbackfunction)associatedwitheachI/Ooperation,whichwillbeinvokedassoonasaneventisproducedandprocessedbytheeventloop.Thestructureofthereactorpatternisshowninthefollowingimage:
Thisiswhathappensinanapplicationusingthereactorpattern:
1. TheapplicationgeneratesanewI/OoperationbysubmittingarequesttotheEventDemultiplexer.Theapplicationalsospecifiesahandler,whichwillbeinvokedwhentheoperationcompletes.SubmittinganewrequesttotheEvent
Demultiplexerisanon-blockingcallanditimmediatelyreturnsthecontrolbacktotheapplication.
2. WhenasetofI/Ooperationscompletes,theEventDemultiplexerpushestheneweventsintotheEventQueue.
3. Atthispoint,theEventLoopiteratesovertheitemsoftheEventQueue.4. Foreachevent,theassociatedhandlerisinvoked.5. Thehandler,whichispartoftheapplicationcode,willgivebackthecontroltothe
EventLoopwhenitsexecutioncompletes(5a).However,newasynchronousoperationsmightberequestedduringtheexecutionofthehandler(5b),causingnewoperationstobeinsertedintheEventDemultiplexer(1),beforethecontrolisgivenbacktotheEventLoop.
6. WhenalltheitemsintheEventQueueareprocessed,theloopwillblockagainontheEventDemultiplexerwhichwillthentriggeranothercycle.
Theasynchronousbehaviorisnowclear:theapplicationexpressestheinteresttoaccessaresourceatonepointintime(withoutblocking)andprovidesahandler,whichwillthenbeinvokedatanotherpointintimewhentheoperationcompletes.
NoteANode.jsapplicationwillexitautomaticallywhentherearenomorependingoperationsintheEventDemultiplexer,andnomoreeventstobeprocessedinsidetheEventQueue.
WecannowdefinethepatternattheheartofNode.js.
NotePattern(reactor):handlesI/Obyblockinguntilneweventsareavailablefromasetofobservedresources,andthenreactingbydispatchingeacheventtoanassociatedhandler.
Thenon-blockingI/OengineofNode.js–libuvEachoperatingsystemhasitsowninterfacefortheEventDemultiplexer:epollonLinux,kqueueonMacOSX,andI/OCompletionPortAPI(IOCP)onWindows.Besidesthat,eachI/Ooperationcanbehavequitedifferentlydependingonthetypeoftheresource,evenwithinthesameOS.Forexample,inUnix,regularfilesystemfilesdonotsupportnon-blockingoperations,so,inordertosimulateanon-blocking
behavior,itisnecessarytouseaseparatethreadoutsidetheEventLoop.Alltheseinconsistenciesacrossandwithinthedifferentoperatingsystemsrequiredahigher-levelabstractiontobebuiltfortheEventDemultiplexer.ThisisexactlywhytheNode.jscoreteamcreatedaClibrarycalledlibuv,withtheobjectivetomakeNode.jscompatiblewithallthemajorplatformsandnormalizethenon-blockingbehaviorofthedifferenttypesofresource;libuvtodayrepresentsthelow-levelI/OengineofNode.js.
Besidesabstractingtheunderlyingsystemcalls,libuvalsoimplementsthereactorpattern,thusprovidinganAPIforcreatingeventloops,managingtheeventqueue,runningasynchronousI/Ooperations,andqueuingothertypesoftasks.
NoteAgreatresourcetolearnmoreaboutlibuvisthefreeonlinebookcreatedbyNikhilMarathe,whichisavailableathttp://nikhilm.github.io/uvbook/.
TherecipeforNode.jsThereactorpatternandlibuvarethebasicbuildingblocksofNode.js,butweneedthefollowingthreeothercomponentstobuildthefullplatform:
Asetofbindingsresponsibleforwrappingandexposinglibuvandotherlow-levelfunctionalitytoJavaScript.V8,theJavaScriptengineoriginallydevelopedbyGooglefortheChromebrowser.ThisisoneofthereasonswhyNode.jsissofastandefficient.V8isacclaimedforitsrevolutionarydesign,itsspeed,andforitsefficientmemorymanagement.AcoreJavaScriptlibrary(callednode-core)thatimplementsthehigh-levelNode.jsAPI.
Finally,thisistherecipeofNode.js,andthefollowingimagerepresentsitsfinalarchitecture:
ThecallbackpatternCallbacksarethematerializationofthehandlersofthereactorpatternandtheyareliterallyoneofthoseimprintsthatgiveNode.jsitsdistinctiveprogrammingstyle.Callbacksarefunctionsthatareinvokedtopropagatetheresultofanoperationandthisisexactlywhatweneedwhendealingwithasynchronousoperations.Theypracticallyreplacetheuseofthereturninstructionthat,asweknow,alwaysexecutessynchronously.JavaScriptisagreatlanguagetorepresentcallbacks,becauseasweknow,functionsarefirstclassobjectsandcanbeeasilyassignedtovariables,passedasarguments,returnedfromanotherfunctioninvocation,orstoredintodatastructures.Also,closuresareanidealconstructforimplementingcallbacks.Withclosures,wecaninfactreferencetheenvironmentinwhichafunctionwascreated,practically,wecanalwaysmaintainthecontextinwhichtheasynchronousoperationwasrequested,nomatterwhenorwhereitscallbackisinvoked.
NoteIfyouneedtorefreshyourknowledgeaboutclosures,youcanrefertothearticleontheMozillaDeveloperNetworkathttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures.
Inthissection,wewillanalyzethisparticularstyleofprogrammingmadeofcallbacksinsteadofthereturninstructions.
Thecontinuation-passingstyleInJavaScript,acallbackisafunctionthatispassedasanargumenttoanotherfunctionandisinvokedwiththeresultwhentheoperationcompletes.Infunctionalprogramming,thiswayofpropagatingtheresultiscalledcontinuation-passingstyle,forbrevity,CPS.Itisageneralconcept,anditisnotalwaysassociatedwithasynchronousoperations.Infact,itsimplyindicatesthataresultispropagatedbypassingittoanotherfunction(thecallback),insteadofdirectlyreturningittothecaller.
Synchronouscontinuation-passingstyleToclarifytheconcept,let'stakealookatasimplesynchronousfunction:
functionadd(a,b){
returna+b;
}
Thereisnothingspecialhere;theresultispassedbacktothecallerusingthereturninstruction;thisisalsocalleddirectstyle,anditrepresentsthemostcommonwayofreturningaresultinsynchronousprogramming.Theequivalentcontinuation-passingstyleoftheprecedingfunctionwouldbeasfollows:
functionadd(a,b,callback){
callback(a+b);
}
Theadd()functionisasynchronousCPSfunction,whichmeansthatitwillreturnavalueonlywhenthecallbackcompletesitsexecution.Thefollowingcodedemonstratesthisstatement:
console.log('before');
add(1,2,function(result){
console.log('Result:'+result);
});
console.log('after');
Sinceadd()issynchronous,thepreviouscodewilltriviallyprintthefollowing:
before
Result:3
after
Asynchronouscontinuation-passingstyleNow,let'sconsiderthecasewheretheadd()functionisasynchronous,whichisasfollows:
functionaddAsync(a,b,callback){
setTimeout(function(){
callback(a+b);
},100);
}
Inthepreviouscode,wesimplyusesetTimeout()tosimulateanasynchronousinvocationofthecallback.Now,let'strytousethisfunctionandseehowtheorderoftheoperationschanges:
console.log('before');
addAsync(1,2,function(result){
console.log('Result:'+result);
});
console.log('after');
Theprecedingcodewillprintthefollowing:
before
after
Result:3
SincesetTimeout()triggersanasynchronousoperation,itwillnotwaitanymoreforthecallbacktobeexecuted,butinstead,itreturnsimmediatelygivingthecontrolbacktoaddAsync(),andthenbacktoitscaller.ThispropertyinNode.jsiscrucial,asitallowsthestacktounwind,andthecontroltobegivenbacktotheeventloopassoonasanasynchronousrequestissent,thusallowinganeweventfromthequeuetobeprocessed.
Thefollowingimageshowshowthisworks:
Whentheasynchronousoperationcompletes,theexecutionisthenresumedstartingfromthecallbackprovidedtotheasynchronousfunctionthatcausedtheunwinding.TheexecutionwillstartfromtheEventLoop,soitwillhaveafreshstack.ThisiswhereJavaScriptcomesinreallyhandy,infact,thankstoclosuresitistrivialtomaintainthecontextofthecalleroftheasynchronousfunction,evenifthecallbackisinvokedatadifferentpointintimeandfromadifferentlocation.
NoteAsynchronousfunctionblocksuntilitcompletesitsoperations.Anasynchronousfunctionreturnsimmediatelyandtheresultispassedtoahandler(inourcase,acallback)atalatercycleoftheeventloop.
Noncontinuation-passingstylecallbacksThereareseveralcircumstancesinwhichthepresenceofacallbackargumentmightmakeyouthinkthatafunctionisasynchronousorisusingacontinuation-passingstyle;that'snotalwaystrue,let'stake,forexample,themap()methodoftheArrayobject:
varresult=[1,5,7].map(function(element){
returnelement–1;
});
Clearly,thecallbackisjustusedtoiterateovertheelementsofthearray,andnottopasstheresultoftheoperation.Infact,theresultisreturnedsynchronouslyusingadirectstyle.TheintentofacallbackisusuallyclearlystatedinthedocumentationoftheAPI.
Synchronousorasynchronous?Wehaveseenhowtheorderoftheinstructionschangesradicallydependingonthenatureofafunction-synchronousorasynchronous.Thishasstrongrepercussionsontheflowoftheentireapplication,bothincorrectnessandefficiency.Thefollowingisananalysisofthesetwoparadigmsandtheirpitfalls.Ingeneral,whatmustbeavoided,iscreatinginconsistencyandconfusionaroundthenatureofanAPI,asdoingsocanleadtoasetofproblemswhichmightbeveryhardtodetectandreproduce.Todriveouranalysis,wewilltakeasexamplethecaseofaninconsistentlyasynchronousfunction.
AnunpredictablefunctionOneofthemostdangeroussituationsistohaveanAPIthatbehavessynchronouslyundercertainconditionsandasynchronouslyunderothers.Let'stakethefollowingcodeasanexample:
varfs=require('fs');
varcache={};
functioninconsistentRead(filename,callback){
if(cache[filename]){
//invokedsynchronously
callback(cache[filename]);
}else{
//asynchronousfunction
fs.readFile(filename,'utf8',function(err,data){
cache[filename]=data;
callback(data);
});
}
}
Theprecedingfunctionusesthecachevariabletostoretheresultsofdifferentfilereadoperations.Pleasebearinmindthatthisisjustanexample,itdoesnothaveerrormanagement,andthecachinglogicitselfissuboptimal.Besidesthis,theprecedingfunctionisdangerousbecauseitbehavesasynchronouslyuntilthecacheisnotset—whichisuntilthefs.readFile()functionreturnsitsresults—butitwillalsobesynchronousforallthesubsequentrequestsforafilealreadyinthecache—triggeringanimmediateinvocationofthecallback.
UnleashingZalgoNow,let'sseehowtheuseofanunpredictablefunction,suchastheonethatwedefinedpreviously,caneasilybreakanapplication.Considerthefollowingcode:
functioncreateFileReader(filename){
varlisteners=[];
inconsistentRead(filename,function(value){
listeners.forEach(function(listener){
listener(value);
});
});
return{
onDataReady:function(listener){
listeners.push(listener);
}
};
}
Whentheprecedingfunctionisinvoked,itcreatesanewobjectthatactsasanotifier,allowingtosetmultiplelistenersforafilereadoperation.Allthelistenerswillbeinvokedatoncewhenthereadoperationcompletesandthedataisavailable.TheprecedingfunctionusesourinconsistentRead()functiontoimplementthisfunctionality.Let'snowtrytousethecreateFileReader()function:
varreader1=createFileReader('data.txt');
reader1.onDataReady(function(data){
console.log('Firstcalldata:'+data);
//...sometimelaterwetrytoreadagainfrom
//thesamefile
varreader2=createFileReader('data.txt');
reader2.onDataReady(function(data){
console.log('Secondcalldata:'+data);
});
});
Theprecedingcodewillprintthefollowingoutput:
Firstcalldata:somedata
Asyoucansee,thecallbackofthesecondoperationisneverinvoked.Let'sseewhy:
Duringthecreationofreader1,ourinconsistentRead()functionbehavesasynchronously,becausethereisnocachedresultavailable.Therefore,wehaveallthetimetoregisterourlistener,asitwillbeinvokedlaterinanothercycleoftheeventloop,whenthereadoperationcompletes.Then,reader2iscreatedinacycleoftheeventloopinwhichthecachefortherequestedfilealreadyexists.Inthiscase,theinnercalltoinconsistentRead()willbesynchronous.So,itscallbackwillbeinvokedimmediately,whichmeansthatalsoallthelistenersofreader2willbeinvokedsynchronously.However,weareregisteringthelistenersafterthecreationofreader2,sotheywillneverbeinvoked.
ThecallbackbehaviorofourinconsistentRead()functionisreallyunpredictable,asitdependsonmanyfactors,suchasthefrequencyofitsinvocation,thefilenamepassedasargument,andtheamountoftimetakentoloadthefile.
Thebugthatwe'vejustseenmightbeextremelycomplicatedtoidentifyandreproduceinarealapplication.Imaginetouseasimilarfunctioninawebserver,wheretherecanbemultipleconcurrentrequests;imagineseeingsomeofthoserequestshanging,withoutanyapparentreasonandwithoutanyerrorbeinglogged.Thisdefinitelyfallsunderthecategoryofnastydefects.
IsaacZ.Schlueter,creatorofnpmandformerNode.jsprojectlead,inoneofhisblogpostscomparedtheuseofthistypeofunpredictablefunctionstounleashingZalgo.Ifyou'renotfamiliarwithZalgo,youareinvitedtofindoutwhatitis.
NoteYoucanfindtheoriginalIsaacZ.Schlueter'spostat
http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony.
UsingsynchronousAPIsThelessontolearnfromtheunleashingZalgoexampleisthatitisimperativeforanAPItoclearlydefineitsnature,eithersynchronousorasynchronous.
OnesuitablefixforourinconsistentRead()function,istomakeittotallysynchronous.ThisispossiblebecauseNode.jsprovidesasetofsynchronousdirectstyleAPIsformostofthebasicI/Ooperations.Forexample,wecanusethefs.readFileSync()functioninplaceofitsasynchronouscounterpart.Thecodewouldnowbeasfollows:
varfs=require('fs');
varcache={};
functionconsistentReadSync(filename){
if(cache[filename]){
returncache[filename];
}else{
cache[filename]=fs.readFileSync(filename,'utf8');
returncache[filename];
}
}
Wecanseethattheentirefunctionwasalsoconvertedtoadirectstyle.Thereisnoreasonforthefunctiontohaveacontinuation-passingstyleifitissynchronous.Infact,wecanstatethatitisalwaysagoodpracticetoimplementasynchronousAPIusingadirectstyle;thiswilleliminateanyconfusionarounditsnatureandwillalsobemoreefficientfromaperformanceperspective.
TipPattern:preferthedirectstyleforpurelysynchronousfunctions.
PleasebearinmindthatchanginganAPIfromCPStoadirectstyle,orfromasynchronoustosynchronous,orviceversamightalsorequireachangetothestyleofallthecodeusingit.Forexample,inourcase,wewillhavetototallychangetheinterfaceofourcreateFileReader()APIandadaptittoworkalwayssynchronously.
Also,usingasynchronousAPIinsteadofanasynchronousonehassomecaveats:
AsynchronousAPImightnotbealwaysavailablefortheneededfunctionality.
AsynchronousAPIwillblocktheeventloopandputtheconcurrentrequestsonhold.ItpracticallybreakstheNode.jsconcurrency,slowingdownthewholeapplication.Wewillseelaterinthebookwhatthisreallymeansforourapplications.
InourconsistentReadSync()function,theriskofblockingtheeventloopispartiallymitigated,becausethesynchronousI/OAPIisinvokedonlyoncepereachfilename,whilethecachedvaluewillbeusedforallthesubsequentinvocations.Ifwehavealimitednumberofstaticfiles,thenusingconsistentReadSync()won'thaveabigeffectonoureventloop.Thingscanchangequicklyifwehavetoreadmanyfilesandonlyonce.UsingsynchronousI/OinNode.jsisstronglydiscouragedinmanycircumstances;however,insomesituations,thismightbetheeasiestandmostefficientsolution.Alwaysevaluateyourspecificusecaseinordertochoosetherightalternative.
TipUseblockingAPIonlywhentheydon'taffecttheabilityoftheapplicationtoserveconcurrentrequests.
DeferredexecutionAnotheralternativeforfixingourinconsistentRead()functionistomakeitpurelyasynchronous.Thetrickhereistoschedulethesynchronouscallbackinvocationtobeexecuted"inthefuture"insteadofbeingrunimmediatelyinthesameeventloopcycle.InNode.js,thisispossibleusingprocess.nextTick(),whichdeferstheexecutionofafunctionuntilthenextpassoftheeventloop.Itsfunctioningisverysimple;ittakesacallbackasanargumentandpushesitonthetopoftheeventqueue,infrontofanypendingI/Oevent,andreturnsimmediately.Thecallbackwillthenbeinvokedassoonastheeventlooprunsagain.
Let'sapplythistechniquetofixourinconsistentRead()functionasfollows:
varfs=require('fs');
varcache={};
functionconsistentReadAsync(filename,callback){
if(cache[filename]){
process.nextTick(function(){
callback(cache[filename]);
});
}else{
//asynchronousfunction
fs.readFile(filename,'utf8',function(err,data){
cache[filename]=data;
callback(data);
});
}
}
Now,ourfunctionisguaranteedtoinvokeitscallbackasynchronously,underanycircumstances.
AnotherAPIfordeferringtheexecutionofcodeissetImmediate(),which—despitethename—mightactuallybeslowerthanprocess.nextTick().Whiletheirpurposeisverysimilar,theirsemanticisquitedifferent.Callbacksdeferredwithprocess.nextTick()runbeforeanyotherI/Oeventisfired,whilewithsetImmediate(),theexecutionisqueuedbehindanyI/Oeventthatisalreadyinthequeue.Sinceprocess.nextTick()runsbeforeanyalreadyscheduledI/O,itmightcauseI/Ostarvationundercertaincircumstances,forexample,arecursiveinvocation;thiscanneverhappenwithsetImmediate().WewilllearntoappreciatethedifferencebetweenthesetwoAPIswhenweanalyzetheuseofdeferredinvocationforrunningsynchronousCPU-boundtaskslaterinthebook.
TipPattern:weguaranteethatacallbackisinvokedasynchronouslybydeferringitsexecutionusingprocess.nextTick().
Node.jscallbackconventionsInNode.js,continuation-passingstyleAPIsandcallbacksfollowasetofspecificconventions.TheseconventionsapplytotheNode.jscoreAPIbuttheyarealsofollowedvirtuallybyeveryuserlandmoduleandapplication.So,it'sveryimportantthatweunderstandthemandmakesurethatwecomplywheneverweneedtodesignanasynchronousAPI.
CallbackscomelastInNode.js,ifafunctionacceptsininputacallback,thishastobepassedasthelastargument.Let'stakethefollowingNode.jscoreAPIasanexample:
fs.readFile(filename,[options],callback)
Asyoucanseefromthesignatureoftheprecedingfunction,thecallbackisalways
putinlastposition,eveninthepresenceofoptionalarguments.Themotivationforthisconventionisthatthefunctioncallismorereadableincasethecallbackisdefinedinplace.
ErrorcomesfirstInCPS,errorsarepropagatedasanyothertypeofresult,whichmeansusingthecallback.InNode.js,anyerrorproducedbyaCPSfunctionisalwayspassedasthefirstargumentofthecallback,andanyactualresultispassedstartingfromthesecondargument.Iftheoperationsucceedswithouterrors,thefirstargumentwillbenullorundefined.Thefollowingcodeshowsyouhowtodefineacallbackcomplyingwiththisconvention:
fs.readFile('foo.txt','utf8',function(err,data){
if(err)
handleError(err);
else
processData(data);
});
Itisagoodpracticetoalwayscheckforthepresenceofanerror,asnotdoingsowillmakeitharderforustodebugourcodeanddiscoverthepossiblepointsoffailures.AnotherimportantconventiontotakeintoaccountisthattheerrormustalwaysbeoftypeError.Thismeansthatsimplestringsornumbersshouldneverbepassedaserrorobjects.
PropagatingerrorsPropagatingerrorsinsynchronous,directstylefunctionsisdonewiththewell-knownthrowcommand,whichcausestheerrortojumpupinthecallstackuntilit'scaught.
InasynchronousCPShowever,propererrorpropagationisdonebysimplypassingtheerrortothenextcallbackintheCPSchain.Thetypicalpatternlooksasfollows:
varfs=require('fs');
functionreadJSON(filename,callback){
fs.readFile(filename,'utf8',function(err,data){
varparsed;
if(err)
//propagatetheerrorandexitthecurrentfunction
returncallback(err);
try{
//parsethefilecontents
parsed=JSON.parse(data);
}catch(err){
//catchparsingerrors
returncallback(err);
}
//noerrors,propagatejustthedata
callback(null,parsed);
});
};
Thedetailtonoticeinthepreviouscodeishowthecallbackisinvokedwhenwewanttopassavalidresultandwhenwewanttopropagateanerror.
UncaughtexceptionsYoumighthaveseenfromthereadJSON()functiondefinedpreviouslythatinordertoavoidanyexceptiontobethrownintothefs.readFile()callback,weputatry-catchblockaroundJSON.parse().Throwinginsideanasynchronouscallback,infact,willcausetheexceptiontojumpuptotheeventloopandneverbepropagatedtothenextcallback.
InNode.js,thisisanunrecoverablestateandtheapplicationwillsimplyshutdownprintingtheerrortothestderrinterface.Todemonstratethis,let'strytoremovethetry-catchblockfromthereadJSON()functiondefinedpreviously:
varfs=require('fs');
functionreadJSONThrows(filename,callback){
fs.readFile(filename,'utf8',function(err,data){
if(err)
returncallback(err);
//noerrors,propagatejustthedata
callback(null,JSON.parse(data));
});
};
Now,inthefunctionwejustdefined,thereisnowayofcatchinganeventualexceptioncomingfromJSON.parse().Let'stry,forexample,toparseaninvalidJSONfilewiththefollowingcode:
readJSONThrows('nonJSON.txt',function(err){
console.log(err);
});
Thiswouldresultintheapplicationbeingabruptlyterminatedandthefollowingexceptionbeingprintedontheconsole:
SyntaxError:Unexpectedtokend
atObject.parse(native)
at[...]/06_uncaught_exceptions/uncaught.js:7:25
atfs.js:266:14
atObject.oncomplete(fs.js:107:15)
Now,ifwelookattheprecedingstacktrace,wewillseethatitstartssomewherefromthefs.jsmodule,practicallyfromthepointatwhichthenativeAPIhascompletedreadingandreturneditsresultbacktothefs.readFile()function,viatheeventloop.Thisclearlyshowsusthattheexceptiontraveledfromourcallbackintothestackthatwesaw,andthenstraightintotheeventloop,whereit'sfinallycaughtandthrownintheconsole.
ThisalsomeansthatwrappingtheinvocationofreadJSONThrows()withatry-catchblockwillnotwork,becausethestackinwhichtheblockoperatesisdifferentfromtheoneinwhichourcallbackisinvoked.Thefollowingcodeshowstheanti-patternthatwejustdescribed:
try{
readJSONThrows('nonJSON.txt',function(err,result){
[...]
});
}catch(err){
console.log('ThiswillnotcatchtheJSONparsing
exception');
}
TheprecedingcatchstatementwillneverreceivetheJSONparsingexception,asitwilltravelbacktothestackinwhichtheexceptionwasthrown,andwejustsawthatthestackendsupintheeventloopandnotwiththefunctionthattriggerstheasynchronousoperation.
Wealreadysaidthattheapplicationisabortedthemomentanexceptionreachestheeventloop;however,westillhavealastchancetoperformsomecleanuporloggingbeforetheapplicationterminates.Infact,whenthishappens,Node.jsemitsaspecialeventcalleduncaughtExceptionjustbeforeexitingtheprocess.Thefollowingcodeshowsasampleusecase:
process.on('uncaughtException',function(err){
console.error('Thiswillcatchatlastthe'+
'JSONparsingexception:'+err.message);
//withoutthis,theapplicationwouldcontinue
process.exit(1);
});
It'simportanttounderstandthatanuncaughtexceptionleavestheapplicationinastatethatisnotguaranteedtobeconsistent,whichcanleadtounforeseeableproblems.Forexample,theremightstillhaveincompleteI/Orequestsrunning,or
closuresmighthavebecomeinconsistent.That'swhyitisalwaysadvised,especiallyinproduction,toexitanywayfromtheapplicationafteranuncaughtexceptionisreceived.
ThemodulesystemanditspatternsModulesarethebricksforstructuringnon-trivialapplications,butalsothemainmechanismtoenforceinformationhidingbykeepingprivateallthefunctionsandvariablesthatarenotexplicitlymarkedtobeexported.Inthissection,wewillintroducetheNode.jsmodulesystemanditsmostcommonusagepatterns.
TherevealingmodulepatternOneofthemajorproblemswithJavaScriptistheabsenceofnamespacing.Programsrunintheglobalscopepollutingitwithdatathatcomesfrombothinternalapplicationcodeanddependencies.Apopulartechniquetosolvethisproblemiscalledrevealingmodulepatternanditlookslikethefollowing:
varmodule=(function(){
varprivateFoo=function(){...};
varprivateVar=[];
varexport={
publicFoo:function(){...},
publicBar:function(){...}
}
returnexport;
})();
Thispatternleveragesaself-invokingfunctiontocreateaprivatescope,exportingonlythepartsthataremeanttobepublic.Intheprecedingcode,themodulevariablecontainsonlytheexportedAPI,whiletherestofthemodulecontentispracticallyinaccessiblefromoutside.Aswewillseeinamoment,theideabehindthispatternisusedasabasefortheNode.jsmodulesystem.
Node.jsmodulesexplainedCommonJSisagroupwiththeaimtostandardizetheJavaScriptecosystem,andoneoftheirmostpopularproposalsiscalledCommonJSmodules.Node.jsbuiltitsmodulesystemontopofthisspecification,withtheadditionofsomecustomextensions.Todescribehowitworks,wecanmakeananalogywiththerevealingmodulepattern,whereeachmodulerunsinaprivatescope,sothateveryvariablethatisdefinedlocallydoesnotpollutetheglobalnamespace.
Ahomemademoduleloader
Toexplainhowthisworks,let'sbuildasimilarsystemfromscratch.Thecodethatfollowscreatesafunctionthatmimicsasubsetofthefunctionalityoftheoriginalrequire()functionofNode.js.
Let'sstartbycreatingafunctionthatloadsthecontentofamodule,wrapsitintoaprivatescope,andevaluatesit:
functionloadModule(filename,module,require){
varwrappedSrc=
'(function(module,exports,require){'+
fs.readFileSync(filename,'utf8')+
'})(module,module.exports,require);';
eval(wrappedSrc);
}
Thesourcecodeofamoduleisessentiallywrappedintoafunction,asitwasfortherevealingmodulepattern.Thedifferencehereisthatwepassalistofvariablestothemodule,inparticular:module,exports,andrequire.Makeanoteofhowtheexportsargumentofthewrappingfunctionisinitializedwiththecontentsofmodule.exports,aswewilltalkaboutthislater.
NotePleasebearinmindthatthisisonlyanexampleandyouwillrarelyneedtoevaluatesomesourcecodeinarealapplication.Featuressuchaseval()orthefunctionsofthevmmodule(http://nodejs.org/api/vm.html)canbeeasilyusedinthewrongwayorwiththewronginput,thusopeningasystemtocodeinjectionattacks.Theyshouldalwaysbeusedwithextremecareoravoidedaltogether.
Let'snowseewhatthesevariablescontainbyimplementingourrequire()function:
varrequire=function(moduleName){
console.log('Requireinvokedformodule:'+moduleName);
varid=require.resolve(moduleName);//[1]
if(require.cache[id]){//[2]
returnrequire.cache[id].exports;
}
//modulemetadata
varmodule={//[3]
exports:{},
id:id
};
//Updatethecache
require.cache[id]=module;//[4]
//loadthemodule
loadModule(id,module,require);//[5]
//returnexportedvariables
returnmodule.exports;//[6]
};
require.cache={};
require.resolve=function(moduleName){
/*resolveafullmoduleidfromthemoduleName*/
}
Theprecedingfunctionsimulatesthebehavioroftheoriginalrequire()functionofNode.js,whichisusedtoloadamodule.Ofcourse,thisisjustforeducativepurposesanditdoesnotaccuratelyorcompletelyreflecttheinternalbehavioroftherealrequire()function,butit'sgreattounderstandtheinternalsoftheNode.jsmodulesystem,howamoduleisdefined,andloaded.Whatourhomemademodulesystemdoesisexplainedasfollows:
1. Amodulenameisacceptedasinputandtheveryfirstthingthatwedoisresolvethefullpathofthemodule,whichwecallid.Thistaskisdelegatedtorequire.resolve(),whichimplementsaspecificresolvingalgorithm(wewilltalkaboutitlater).
2. Ifthemodulewasalreadyloadedinthepast,itshouldbeavailableinthecache.Inthiscase,wejustreturnitimmediately.
3. Ifthemodulewasnotyetloaded,wesetuptheenvironmentforthefirstload.Inparticular,wecreateamoduleobjectthatcontainsanexportspropertyinitializedwithanemptyobjectliteral.ThispropertywillbeusedbythecodeofthemoduletoexportanypublicAPI.
4. Themoduleobjectiscached.5. Themodulesourcecodeisreadfromitsfileandthecodeisevaluated,aswe
haveseenbefore.Weprovidetothemodule,themoduleobjectthatwejustcreated,andareferencetotherequire()function.ThemoduleexportsitspublicAPIbymanipulatingorreplacingthemodule.exportsobject.
6. Finally,thecontentofmodule.exports,whichrepresentsthepublicAPIofthemodule,isreturnedtothecaller.
Aswesee,thereisnothingmagicalbehindtheworkingsoftheNode.jsmodulesystem;thetrickisallinthewrapperwecreatearoundamodule'ssourcecodeandtheartificialenvironmentinwhichwerunit.
DefiningamoduleBylookingathowourhomemaderequire()functionworks,weshouldnowknowhowtodefineamodule.Thefollowingcodegivesusanexample:
//loadanotherdependency
vardependency=require('./anotherModule');
//aprivatefunction
functionlog(){
console.log('Welldone'+dependency.username);
}
//theAPItobeexportedforpublicuse
module.exports.run=function(){
log();
};
Theessentialconcepttorememberisthateverythinginsideamoduleisprivateunlessit'sassignedtothemodule.exportsvariable.Thecontentsofthisvariablearethencachedandreturnedwhenthemoduleisloadedusingrequire().
DefiningglobalsEvenifallthevariablesandfunctionsthataredeclaredinamodulearedefinedinitslocalscope,itisstillpossibletodefineaglobalvariable.Infact,themodulesystemexposesaspecialvariablecalledglobal,whichcanbeusedforthispurpose.Everythingthatisassignedtothisvariablewillendupautomaticallyintheglobalscope.
NotePleasenotethatpollutingtheglobalscopeisconsideredabadpracticeandnullifiestheadvantageofhavingamodulesystem.So,useitonlyifyoureallyknowwhatyouaredoing.
module.exportsvsexportsFormanydeveloperswhoarenotyetfamiliarwithNode.js,acommonsourceofconfusionisthedifferencebetweenusingexportsandmodule.exportstoexposeapublicAPI.Thecodeofourhomemaderequirefunctionshouldagainclearanydoubt.Thevariableexportsisjustareferencetotheinitialvalueofmodule.exports;wehaveseenthatsuchavalueisessentiallyasimpleobjectliteralcreatedbeforethemoduleisloaded.
Thismeansthatwecanonlyattachnewpropertiestotheobjectreferencedbytheexportsvariable,asshowninthefollowingcode:
exports.hello=function(){
console.log('Hello');
}
Reassigningtheexportsvariabledoesn'thaveanyeffect,becauseitdoesn'tchangethecontentsofmodule.exports,itwillonlyreassignthevariableitself.Thefollowingcodeisthereforewrong:
exports=function(){
console.log('Hello');
}
Ifwewanttoexportsomethingotherthananobjectliteral,asforexampleafunction,aninstance,orevenastring,wehavetoreassignmodule.exportsasfollows:
module.exports=function(){
console.log('Hello');
}
requireissynchronousAnotherimportantdetailthatweshouldtakeintoaccountisthatourhomemaderequirefunctionissynchronous.Infact,itreturnsthemodulecontentsusingasimpledirectstyle,andnocallbackisrequired.ThisistruefortheoriginalNode.jsrequire()functiontoo.Asaconsequence,anyassignmenttomodule.exportmustbesynchronousaswell.Forexample,thefollowingcodeisincorrect:
setTimeout(function(){
module.exports=function(){...};
},100);
Thispropertyhasimportantrepercussionsinthewaywedefinemodules,asitlimitsustomostlyusingsynchronouscodeduringthedefinitionofamodule.ThisisactuallyoneofthemostimportantreasonswhythecoreNode.jslibrariesoffersynchronousAPIsasanalternativetomostoftheasynchronousones.
Ifweneedsomeasynchronousinitializationstepsforamodule,wecanalwaysdefineandexportanuninitializedmodulethatisinitializedasynchronouslyatalatertime.Theproblemwiththisapproachthough,isthatloadingsuchamoduleusingrequiredoesnotguaranteethatit'sreadytobeused.InChapter6,Recipes,wewillanalyzethisproblemindetailandwewillpresentsomepatternstosolvethisissueelegantly.
Note
Forthesakeofcuriosity,youmightwanttoknowthatinitsearlydays,Node.jsusedtohaveanasynchronousversionofrequire(),butitwassoonremovedbecauseitwasovercomplicatingafunctionalitythatwasactuallymeanttobeusedonlyatinitializationtime,andwhereasynchronousI/Obringsmorecomplexitiesthanadvantages.
TheresolvingalgorithmThetermdependencyhell,describesasituationwherebythedependenciesofasoftware,inturndependonashareddependency,butrequiredifferentincompatibleversions.Node.jssolvesthisproblemelegantlybyloadingadifferentversionofamoduledependingonwherethemoduleisloadedfrom.Allthemeritsofthisfeaturegotonpmandalsototheresolvingalgorithmusedintherequirefunction.
Let'snowgiveaquickoverviewofthisalgorithm.Aswesaw,theresolve()functiontakesamodulename(whichwewillcallhere,moduleName)asinputanditreturnsthefullpathofthemodule.Thispathisthenusedtoloaditscodeandalsotoidentifythemoduleuniquely.Theresolvingalgorithmcanbedividedintothefollowingthreemajorbranches:
Filemodules:IfmoduleNamestartswith"/"it'sconsideredalreadyanabsolutepathtothemoduleandit'sreturnedasitis.Ifitstartswith"./",thenmoduleNameisconsideredarelativepath,whichiscalculatedstartingfromtherequiringmodule.Coremodules:IfmoduleNameisnotprefixedwith"/"or"./",thealgorithmwillfirsttrytosearchwithinthecoreNode.jsmodules.Packagemodules:IfnocoremoduleisfoundmatchingmoduleName,thenthesearchcontinuesbylookingforamatchingmoduleintothefirstnode_modulesdirectorythatisfoundnavigatingupinthedirectorystructurestartingfromtherequiringmodule.Thealgorithmcontinuestosearchforamatchbylookingintothenextnode_modulesdirectoryupinthedirectorytree,untilitreachestherootofthefilesystem.
Forfileandpackagemodules,boththeindividualfilesanddirectoriescanmatchmoduleName.Inparticular,thealgorithmwilltrytomatchthefollowing:
<moduleName>.js
<moduleName>/index.js
Thedirectory/filespecifiedinthemainpropertyof<moduleName>/package.json
Note
Thecomplete,formaldocumentationoftheresolvingalgorithmcanbefoundathttp://nodejs.org/api/modules.html#modules_all_together.
Thenode_modulesdirectoryisactuallywherenpminstallsthedependenciesofeachpackage.Thismeansthat,basedonthealgorithmwejustdescribed,eachpackagecanhaveitsownprivatedependencies.Forexample,considerthefollowingdirectorystructure:
myApp
├──foo.js
└──node_modules
├──depA
│└──index.js
├──depB
│├──bar.js
│└──node_modules
│└──depA
│└──index.js
└──depC
├──foobar.js
└──node_modules
└──depA
└──index.js
Intheprecedingexample,myApp,depB,anddepCalldependondepA;however,theyallhavetheirownprivateversionofthedependency!Followingtherulesoftheresolvingalgorithm,usingrequire('depA')willloadadifferentfiledependingonthemodulethatrequiresit,forexample:
Callingrequire('depA')from/myApp/foo.jswillload/myApp/node_modules/depA/index.js
Callingrequire('depA')from/myApp/node_modules/depB/bar.jswillload/myApp/node_modules/depB/node_modules/depA/index.js
Callingrequire('depA')from/myApp/node_modules/depC/foobar.jswillload/myApp/node_modules/depC/node_modules/depA/index.js
TheresolvingalgorithmisthemagicbehindtherobustnessoftheNode.jsdependencymanagement,andiswhatmakesitpossibletohavehundredsoreventhousandsofpackagesinanapplicationwithouthavingcollisionsorproblemsofversioncompatibility.
NoteTheresolvingalgorithmisappliedtransparentlyforuswhenweinvokerequire();
however,ifneeded,itcanstillbeuseddirectlybyanymodulebysimplyinvokingrequire.resolve().
ThemodulecacheEachmoduleisloadedandevaluatedonlythefirsttimeitisrequired,sinceanysubsequentcallofrequire()willsimplyreturnthecachedversion.Thisshouldresultclearbylookingatthecodeofourhomemaderequirefunction.Cachingiscrucialforperformances,butitalsohassomeimportantfunctionalimplications:
ItmakesitpossibletohavecycleswithinmoduledependenciesItguarantees,tosomeextent,thatalwaysthesameinstanceisreturnedwhenrequiringthesamemodulefromwithinagivenpackage
Themodulecacheisexposedintherequire.cachevariable,soitispossibletodirectlyaccessitifneeded.Acommonusecaseistoinvalidateanycachedmodulebydeletingtherelativekeyintherequire.cachevariable,apracticeveryusefulduringtestingbutverydangerousifappliedinnormalcircumstances.
CyclesManyconsidercirculardependenciesasanintrinsicdesignissue,butitissomethingwhichmightactuallyhappeninarealproject,soit'susefulforustoknowatleasthowthisworksinNode.js.Ifwelookagainatourhomemaderequire()function,weimmediatelygetaglimpseofhowthismightworkandwhatareitscaveats.
Supposewehavetwomodulesdefinedasfollows:
Modulea.js:
exports.loaded=false;
varb=require('./b');
module.exports={
bWasLoaded:b.loaded,
loaded:true
};
Moduleb.js:
exports.loaded=false;
vara=require('./a');
module.exports={
aWasLoaded:a.loaded,
loaded:true
};
Now,let'strytoloadthesefromanothermodule,main.js,asfollows:
vara=require('./a');
varb=require('./b');
console.log(a);
console.log(b);
Theprecedingcodewillprintthefollowingoutput:
{bWasLoaded:true,loaded:true}
{aWasLoaded:false,loaded:true}
Thisresultrevealsthecaveatsofcirculardependencies.Whileboththemodulesarecompletelyinitializedthemomenttheyarerequiredfromthemainmodule,thea.jsmodulewillbeincompletewhenitisloadedfromb.js.Inparticular,itsstatewillbetheonethatitreachedthemomentitrequiredb.js.Thisbehaviorshouldringanotherbell,whichwillbeconfirmedifweswaptheorderinwhichthetwomodulesarerequiredinmain.js.
Ifyoutryit,youwillseethatthistimeitwillbethemodulea.jsthatwillreceiveanincompleteversionofb.js.Weunderstandnowthatthiscanbecomequiteafuzzybusinessifwelosecontrolofwhichmoduleisloadedfirst,whichcanhappenquiteeasilyiftheprojectisbigenough.
ModuledefinitionpatternsThemodulesystem,besidesbeingamechanismforloadingdependencies,isalsoatoolfordefiningAPIs.AsforanyotherproblemrelatedtoAPIdesign,themainfactortoconsideristhebalancebetweenprivateandpublicfunctionality.TheaimistomaximizeinformationhidingandAPIusability,whilebalancingthesewithothersoftwarequalitieslikeextensibilityandcodereuse.
Inthissection,wewillanalyzesomeofthemostpopularpatternsfordefiningmodulesinNode.js;eachonehasitsownbalanceofinformationhiding,extensibility,andcodereuse.
NamedexportsThemostbasicmethodforexposingapublicAPIisusingnamedexports,whichconsistsinassigningallthevalueswewanttomakepublictopropertiesoftheobjectreferencedbyexports(ormodule.exports).Inthisway,theresultingexportedobjectbecomesacontainerornamespaceforasetofrelatedfunctionality.
Thefollowingcodeshowsamoduleimplementingthispattern:
//filelogger.js
exports.info=function(message){
console.log('info:'+message);
};
exports.verbose=function(message){
console.log('verbose:'+message);
};
Theexportedfunctionsarethenavailableaspropertiesoftheloadedmodule,asshowninthefollowingcode:
//filemain.js
varlogger=require('./logger');
logger.info('Thisisaninformationalmessage');
logger.verbose('Thisisaverbosemessage');
MostoftheNode.jscoremodulesusethispattern.
NoteTheCommonJSspecificationonlyallowstheuseoftheexportsvariabletoexposepublicmembers.Therefore,thenamedexportspatternistheonlyonethatisreallycompatiblewiththeCommonJSspecification.Theuseofmodule.exportsisanextensionprovidedbyNode.jstosupportabroaderrangeofmoduledefinitionpatterns,asthosewearegoingtoseenext.
ExportingafunctionOneofthemostpopularmoduledefinitionpatternsconsistsinreassigningthewholemodule.exportsvariabletoafunction.Itsmainstrengthit'sthefactthatitexposesonlyasinglefunctionality,whichprovidesaclearentrypointforthemodule,andmakesitsimpletounderstandanduse;italsohonorstheprincipleofsmallsurfaceareaverywell.Thiswayofdefiningmodulesisalsoknowninthecommunityassubstackpattern,afteroneofitsmostprolificadopters,JamesHalliday(nicknamesubstack).Thefollowingcodeisanexampleofthispattern:
//filelogger.js
module.exports=function(message){
console.log('info:'+message);
};
ApossibleextensionofthispatternisusingtheexportedfunctionasnamespaceforotherpublicAPIs.Thisisaverypowerfulcombination,becauseitstillgivesthemoduletheclarityofasingleentrypoint(themainexportedfunction),butitalsoallowsustoexposeotherfunctionalitiesthathavesecondaryormoreadvancedusecases.Thefollowingcodeshowsyouhowtoextendthemodulewedefinedpreviouslybyusingtheexportedfunctionasanamespace:
module.exports.verbose=function(message){
console.log('verbose:'+message);
};
Thefollowingcodedemonstrateshowtousethemodulethatwejustdefined:
//filemain.js
varlogger=require('./logger');
logger('Thisisaninformationalmessage');
logger.verbose('Thisisaverbosemessage');
Eventhoughexportingjustafunctionmightseemalimitation,inreality,it'saperfectwaytoputtheemphasisonasinglefunctionality—themostimportantforthemodule—whilegivinglessvisibilitytosecondaryaspects,whichareinsteadexposedaspropertiesoftheexportedfunctionitself.
NotePattern(substack):exposethemainfunctionalityofamodulebyexportingonlyonefunction.Usetheexportedfunctionasnamespacetoexposeanyauxiliaryfunctionality.
ExportingaconstructorAmodulethatexportsaconstructorisaspecializationofamodulethatexportsafunction.Thedifferenceisthatwiththisnewpattern,weallowtheusertocreatenewinstancesusingtheconstructor,butwealsogivethemtheabilitytoextenditsprototypeandforgenewclasses.Thefollowingisanexampleofthispattern:
//filelogger.js
functionLogger(name){
this.name=name;
};
Logger.prototype.log=function(message){
console.log('['+this.name+']'+message);
};
Logger.prototype.info=function(message){
this.log('info:'+message);
};
Logger.prototype.verbose=function(message){
this.log('verbose:'+message);
};
module.exports=Logger;
And,wecanusetheprecedingmoduleasfollows:
//filelogger.js
varLogger=require('./logger');
vardbLogger=newLogger('DB');
dbLogger.info('Thisisaninformationalmessage');
varaccessLogger=newLogger('ACCESS');
accessLogger.verbose('Thisisaverbosemessage');
Exportingaconstructorstillprovidesasingleentrypointforthemodule,butcomparedtothesubstackpattern,itexposesalotmoreofthemoduleinternals;howeverontheothersideitallowsmuchmorepowerwhenitcomestoextendingitsfunctionality.
Avariationofthispatternconsistsinapplyingaguardagainstinvocationsthatdon'tusethenewinstruction.Thislittletrickallowsustouseourmoduleasafactory.Thefollowingcodeshowsyouhowthisworks:
functionLogger(name){
if(!(thisinstanceofLogger)){
returnnewLogger(name);
}
this.name=name;
};
Thetrickissimple;wecheckwhetherthisexistsandisaninstanceofLogger.Ifanyoftheseconditionsisfalse,itmeansthattheLogger()functionwasinvokedwithoutusingnew,soweproceedwithcreatingthenewinstanceproperlyandreturningittothecaller.Thistechniqueallowsustousethemodulealsoasafactory,asshowninthefollowingcode:
//filelogger.js
varLogger=require('./logger');
vardbLogger=Logger('DB');
accessLogger.verbose('Thisisaverbosemessage');
Exportinganinstance
Wecanleveragethecachingmechanismofrequire()toeasilydefinestatefulinstances—objectswithastatecreatedfromaconstructororafactory,whichcanbesharedacrossdifferentmodules.Thefollowingcodeshowsanexampleofthispattern:
//filelogger.js
functionLogger(name){
this.count=0;
this.name=name;
};
Logger.prototype.log=function(message){
this.count++;
console.log('['+this.name+']'+message);
};
module.exports=newLogger('DEFAULT');
Thisnewlydefinedmodulecanthenbeusedasfollows:
//filemain.js
varlogger=require('./logger');
logger.log('Thisisaninformationalmessage');
Becausethemoduleiscached,everymodulethatrequirestheloggermodulewillactuallyalwaysretrievethesameinstanceoftheobject,thussharingitsstate.ThispatternisverymuchlikecreatingaSingleton,however,itdoesnotguaranteetheuniquenessoftheinstanceacrosstheentireapplication,asithappensinthetraditionalSingletonpattern.Whenanalyzingtheresolvingalgorithm,wehaveseeninfact,thatamodulemightbeinstalledmultipletimesinsidethedependencytreeofanapplication.Thisresultswithmultipleinstancesofthesamelogicalmodule,allrunninginthecontextofthesameNode.jsapplication.InChapter5,WiringModules,wewillanalyzetheconsequencesofexportingstatefulinstancesandsomeofthepatternswecanuseasalternatives.
Anextensiontothepatternwejustdescribed,consistsinexposingtheconstructorusedtocreatetheinstance,inadditiontotheinstanceitself.Thisallowstheusertocreatenewinstancesofthesameobject,oreventoextenditifnecessary.Toenablethis,wejustneedtoassignanewpropertytotheinstance,asshowninthefollowinglineofcode:
module.exports.Logger=Logger;
Then,wecanusetheexportedconstructortocreateotherinstancesoftheclass,asfollows:
varcustomLogger=newlogger.Logger('CUSTOM');
customLogger.log('Thisisaninformationalmessage');
Fromausabilityperspective,thisissimilartousinganexportedfunctionasnamespace;themoduleexportsthedefaultinstanceofanobject—thepieceoffunctionalitywemightwanttousemostofthetime—whilemoreadvancedfeatures,suchastheabilitytocreatenewinstancesorextendtheobject,arestillmadeavailablethroughlessexposedproperties.
ModifyingothermodulesortheglobalscopeAmodulecanevenexportnothing.Thiscanlookabitoutofplace,however,weshouldnotforgetthatamodulecanmodifytheglobalscopeandanyobjectinit,includingothermodulesinthecache.Pleasenotethattheseareingeneralconsideredbadpractices,butsincethispatterncanbeusefulandsafeundersomecircumstances(forexample,fortesting)andissometimesusedinthewild,itisworthtoknowandunderstandit.So,wesaidamodulecanmodifyothermodulesorobjectsintheglobalscope.Well,thisiscalledmonkeypatching,whichgenerallyreferstothepracticeofmodifyingtheexistingobjectsatruntimetochangeorextendtheirbehaviorortoapplytemporaryfixes.
Thefollowingexampleshowsyouhowwecanaddanewfunctiontoanothermodule:
//filepatcher.js
//./loggerisanothermodule
require('./logger').customMessage=function(){
console.log('Thisisanewfunctionality');
};
Usingournewpatchermodulewouldbeaseasyaswritingthefollowingcode:
//filemain.js
require('./patcher');
varlogger=require('./logger');
logger.customMessage();
Intheprecedingcode,patchermustberequiredbeforeusingtheloggermoduleforthefirsttimeinordertoallowthepatchtobeapplied.
Thetechniquesdescribedherearealldangerousonestoapply.Themainconcernisthat,tohaveamodulethatmodifiestheglobalnamespaceorothermodulesisanoperationwithsideeffects.Inotherwords,itaffectsthestateofentitiesoutsidetheirscope,whichcanhaveconsequencesthatarenotalwayspredictable,especiallywhenmultiplemodulesinteractwiththesameentities.Imaginetohavetwodifferent
modulestryingtosetthesameglobalvariable,ormodifyingthesamepropertyofthesamemodule;theeffectsmightbeunpredictable(whichmodulewins?),butmostimportantlyitwouldhaverepercussionsontheentireapplication.
TheobserverpatternAnotherimportantandfundamentalpatternusedinNode.jsistheobserverpattern.Togetherwithreactor,callbacks,andmodules,thisisoneofthepillarsoftheplatformandanabsoluteprerequisiteforusingmanynode-coreanduserlandmodules.
ObserverisanidealsolutionformodelingthereactivenatureofNode.js,andaperfectcomplementforcallbacks.Let'sgiveaformaldefinitionasfollows:
NotePattern(observer):definesanobject(calledsubject),whichcannotifyasetofobservers(orlisteners),whenachangeinitsstatehappens.
Themaindifferencefromthecallbackpatternisthatthesubjectcanactuallynotifymultipleobservers,whileatraditionalcontinuation-passingstylecallbackwillusuallypropagateitsresulttoonlyonelistener,thecallback.
TheEventEmitterIntraditionalobject-orientedprogramming,theobserverpatternrequiresinterfaces,concreteclasses,andahierarchy;inNode.js,allbecomesmuchsimpler.TheobserverpatternisalreadybuiltintothecoreandisavailablethroughtheEventEmitterclass.TheEventEmitterclassallowsustoregisteroneormorefunctionsaslisteners,whichwillbeinvokedwhenaparticulareventtypeisfired.Thefollowingimagevisuallyexplainstheconcept:
TheEventEmitterisaprototype,anditisexportedfromtheeventscoremodule.Thefollowingcodeshowshowwecanobtainareferencetoit:
varEventEmitter=require('events').EventEmitter;
vareeInstance=newEventEmitter();
TheessentialmethodsoftheEventEmitteraregivenasfollows:
on(event,listener):Thismethodallowsyoutoregisteranewlistener(afunction)forthegiveneventtype(astring)once(event,listener):Thismethodregistersanewlistener,whichisthenremovedaftertheeventisemittedforthefirsttimeemit(event,[arg1],[…]):ThismethodproducesaneweventandprovidesadditionalargumentstobepassedtothelistenersremoveListener(event,listener):Thismethodremovesalistenerforthespecifiedeventtype
AlltheprecedingmethodswillreturntheEventEmitterinstancetoallowchaining.Thelistenerfunctionhasthesignature,function([arg1],[…]),soitsimplyacceptstheargumentsprovidedthemomenttheeventisemitted.Insidethelistener,thisreferstotheinstanceoftheEventEmitterthatproducestheevent.
WecanalreadyseethatthereisabigdifferencebetweenalistenerandatraditionalNode.jscallback;inparticular,thefirstargumentisnotanerror,butitcanbeanydatapassedtoemit()atthemomentofitsinvocation.
CreateanduseanEventEmitterLet'sseehowwecanuseanEventEmitterinpractice.Thesimplestwayistocreateanewinstanceanduseitdirectly.Thefollowingcodeshowsafunction,whichusesanEventEmittertonotifyitssubscribersinrealtimewhenaparticularpatternisfoundinalistoffiles:
varEventEmitter=require('events').EventEmitter;
varfs=require('fs');
functionfindPattern(files,regex){
varemitter=newEventEmitter();
files.forEach(function(file){
fs.readFile(file,'utf8',function(err,content){
if(err)
returnemitter.emit('error',err);
emitter.emit('fileread',file);
varmatch=null;
if(match=content.match(regex))
match.forEach(function(elem){
emitter.emit('found',file,elem);
});
});
});
returnemitter;
}
TheEventEmittercreatedbytheprecedingfunctionwillproducethefollowingthreeevents:
fileread:Thiseventoccurswhenafileisreadfound:Thiseventoccurswhenamatchhasbeenfounderror:Thiseventoccurswhenanerrorhasoccurredduringthereadingofthefile
Let'sseenowhowourfindPattern()functioncanbeused:
findPattern(
['fileA.txt','fileB.json'],
/hello\w+/g
)
.on('fileread',function(file){
console.log(file+'wasread');
})
.on('found',function(file,match){
console.log('Matched"'+match+'"infile'+file);
})
.on('error',function(err){
console.log('Erroremitted:'+err.message);
});
Intheprecedingexample,weregisteredalistenerforeachofthethreeeventtypesproducedbytheEventEmitterwhichwascreatedbyourfindPattern()function.
PropagatingerrorsTheEventEmitter-asithappensforcallbacks-cannotjustthrowexceptionswhenanerrorconditionoccurs,astheywouldbelostintheeventloopiftheeventisemittedasynchronously.Instead,theconventionistoemitaspecialevent,callederror,andtopassanErrorobjectasanargument.That'sexactlywhatwearedoinginthefindPattern()functionthatwedefinedearlier.
NoteItisalwaysagoodpracticetoregisteralistenerfortheerrorevent,asNode.jswilltreatitinaspecialwayandwillautomaticallythrowanexceptionandexitfromtheprogramifnoassociatedlistenerisfound.
MakeanyobjectobservableSometimes,creatinganewobservableobjectdirectlyfromtheEventEmitterclassisnotenough,asthismakesitimpracticaltoprovidefunctionalitythatgoesbeyondthemereproductionofnewevents.Itismorecommon,infact,tohavetheneedtomakeagenericobjectobservable;thisispossiblebyextendingtheEventEmitterclass.
Todemonstratethispattern,let'strytoimplementthefunctionalityofthefindPattern()functioninanobjectasfollows:
varEventEmitter=require('events').EventEmitter;
varutil=require('util');
varfs=require('fs');
functionFindPattern(regex){
EventEmitter.call(this);
this.regex=regex;
this.files=[];
}
util.inherits(FindPattern,EventEmitter);
FindPattern.prototype.addFile=function(file){
this.files.push(file);
returnthis;
};
FindPattern.prototype.find=function(){
varself=this;
self.files.forEach(function(file){
fs.readFile(file,'utf8',function(err,content){
if(err)
returnself.emit('error',err);
self.emit('fileread',file);
varmatch=null;
if(match=content.match(self.regex))
match.forEach(function(elem){
self.emit('found',file,elem);
});
});
});
returnthis;
};
TheFindPatternprototypethatwedefinedextendstheEventEmitterusingtheinherits()functionprovidedbythecoremoduleutil.Thisway,itbecomesafull-fledgedobservableclass.Thefollowingisanexampleofitsusage:
varfindPatternObject=newFindPattern(/hello\w+/);
findPatternObject
.addFile('fileA.txt')
.addFile('fileB.json')
.find()
.on('found',function(file,match){
console.log('Matched"'+match+'"infile'+file);
})
.on('error',function(err){
console.log('Erroremitted'+err.message);
});
WecannowseehowtheFindPatternobjecthasafullsetofmethods,inadditiontobeingobservablebyinheritingthefunctionalityoftheEventEmitter.
ThisisaprettycommonpatternintheNode.jsecosystem,forexample,theServerobjectofthecorehttpmoduledefinesmethodssuchaslisten(),close(),setTimeout(),andinternallyitalsoinheritsfromtheEventEmitterfunction,thusallowingittoproduceevents,suchasrequest,whenanewrequestisreceived,orconnection,whenanewconnectionisestablished,orclosed,whentheserverisclosed.
OthernotableexamplesofobjectsextendingtheEventEmitterareNode.jsstreams.WewillanalyzestreamsinmoredetailinChapter3,CodingwithStreams.
SynchronousandasynchronouseventsAswithcallbacks,eventscanbeemittedsynchronouslyorasynchronously,anditiscrucialthatwenevermixthetwoapproachesinthesameEventEmitter,butevenmoreimportantly,whenemittingthesameeventtype,toavoidtoproducethesameproblemsthatwedescribedintheUnleashingZalgosection.
Themaindifferencebetweenemittingsynchronousorasynchronouseventsliesinthewaylistenerscanberegistered.Whentheeventsareemittedasynchronously,theuserhasallthetimetoregisternewlistenersevenaftertheEventEmitterisinitialized,becausetheeventsareguaranteednottobefireduntilthenextcycleoftheeventloop.That'sexactlywhatishappeninginthefindPattern()function.WedefinedthisfunctionpreviouslyanditrepresentsacommonapproachthatisusedinmostNode.jsmodules.
Onthecontrary,emittingeventssynchronouslyrequiresthatallthelistenersareregisteredbeforetheEventEmitterfunctionstartstoemitanyevent.Let'slookatanexample:
functionSyncEmit(){
this.emit('ready');
}
util.inherits(SyncEmit,EventEmitter);
varsyncEmit=newSyncEmit();
syncEmit.on('ready',function(){
console.log('Objectisreadytobeused');
});
Ifthereadyeventwasemittedasynchronously,thenthepreviouscodewouldworkperfectly;however,theeventisproducedsynchronouslyandthelistenerisregisteredaftertheeventwasalreadysent,sotheresultisthatthelistenerisneverinvoked;thecodewillprintnothingtotheconsole.
Contrarilytocallbacks,therearesituationswhereusinganEventEmitterinasynchronousfashionmakessense,givenitsdifferentpurpose.Forthisreason,it'sveryimportanttoclearlyhighlightthebehaviorofourEventEmitterinitsdocumentationtoavoidconfusion,andpotentiallyawrongusage.
EventEmittervsCallbacksAcommondilemmawhendefininganasynchronousAPIistocheckwhethertouseanEventEmitterorsimplyacceptacallback.Thegeneraldifferentiatingruleissemantic:callbacksshouldbeusedwhenaresultmustbereturnedinan
asynchronousway;eventsshouldinsteadbeusedwhenthereisaneedtocommunicatethatsomethinghasjusthappened.
Butbesidesthissimpleprinciple,alotofconfusionisgeneratedfromthefactthatthetwoparadigmsaremostofthetimeequivalentandallowyoutoachievethesameresults.Considerthefollowingcodeforanexample:
functionhelloEvents(){
vareventEmitter=newEventEmitter();
setTimeout(function(){
eventEmitter.emit('hello','world');
},100);
returneventEmitter;
}
functionhelloCallback(callback){
setTimeout(function(){
callback('hello','world');
},100);
}
ThetwofunctionshelloEvents()andhelloCallback()canbeconsideredequivalentintermsoffunctionality;thefirstcommunicatesthecompletionofthetimeoutusinganevent,thesecondusesacallbacktonotifythecallerinstead,passingtheeventtypeasanargument.Butwhatreallydifferentiatesthemisthereadability,thesemantic,andtheamountofcodethatisrequiredtobeimplementedorused.Whilewecannotgiveadeterministicsetofrulestochoosebetweenoneortheotherstyle,wecancertainlyprovidesomehintstohelptakethedecision.
Asafirstobservation,wecansaythatcallbackshavesomelimitationswhenitcomestosupportingdifferenttypesofevents.Infact,wecanstilldifferentiatebetweenmultipleeventsbypassingthetypeasanargumentofthecallback,orbyacceptingseveralcallbacks,oneforeachsupportedevent.However,thiscannotexactlybeconsideredanelegantAPI.Inthissituation,anEventEmittercangiveabetterinterfaceandleanercode.
AnothercasewheretheEventEmittermightbepreferableiswhenthesameeventcanoccurmultipletimes,ornotoccuratall.Acallback,infact,isexpectedtobeinvokedexactlyonce,whethertheoperationissuccessfulornot.Thefactthatwehaveapossiblyrepeatingcircumstanceshouldletusthinkagainaboutthesemanticnatureoftheoccurrence,whichismoresimilartoaneventthathastobecommunicatedratherthanaresult;inthiscaseanEventEmitteristhepreferredchoice.
Lastly,anAPIusingcallbackscannotifyonlythatparticularcallback,whileusinganEventEmitterfunctionit'spossibleformultiplelistenerstoreceivethesamenotification.
CombinecallbacksandEventEmitterTherearealsosomecircumstanceswhereanEventEmittercanbeusedinconjunctionwithacallback.Thispatternisextremelyusefulwhenwewanttoimplementtheprincipleofsmallsurfaceareabyexportingatraditionalasynchronousfunctionasthemainfunctionality,whilestillprovidingricherfeatures,andmorecontrolbyreturninganEventEmitter.Oneexampleofthispatternisofferedbythenode-globmodule(https://npmjs.org/package/glob),alibrarythatperformsglob-stylefilesearches.Themainentrypointofthemoduleisthefunctionitexports,whichhasthefollowingsignature:
glob(pattern,[options],callback)
Thefunctiontakespatternasthefirstargument,asetofoptions,andacallbackfunctionwhichisinvokedwiththelistofallthefilesmatchingtheprovidedpattern.Atthesametime,thefunctionreturnsanEventEmitterthatprovidesamorefine-grainedreportoverthestateoftheprocess.Forexample,itispossibletobenotifiedinreal-timewhenamatchoccursbylisteningtothematchevent,toobtainthelistofallthematchedfileswiththeendevent,ortoknowwhethertheprocesswasmanuallyabortedbylisteningtotheabortevent.Thefollowingcodeshowshowthislooks:
varglob=require('glob');
glob('data/*.txt',function(error,files){
console.log('Allfilesfound:'+JSON.stringify(files));
}).on('match',function(match){
console.log('Matchfound:'+match);
});
Aswecansee,thepracticeofexposingasimple,clean,andminimalentrypointwhilestillprovidingmoreadvancedorlessimportantfeatureswithsecondarymeansisquitecommoninNode.js,andcombiningEventEmitterwithtraditionalcallbacksisoneofthewaystoachievethat.
TipPattern:createafunctionthatacceptsacallbackandreturnsanEventEmitter,thusprovidingasimpleandclearentrypointforthemainfunctionality,whileemittingmorefine-grainedeventsusingtheEventEmitter.
SummaryInthischapter,wehaveseenhowtheNode.jsplatformisbasedonafewimportantprinciplesthatprovidethefoundationtobuildefficientandreusablecode.Thephilosophyandthedesignchoicesbehindtheplatformhave,infact,astronginfluenceonthestructureandbehaviorofeveryapplicationandmodulewecreate.Often,foradevelopermovingfromanothertechnology,theseprinciplesmightseemunfamiliarandtheusualinstinctivereactionistofightthechangebytryingtofindmorefamiliarpatternsinsideaworldwhichinrealityrequiresarealshiftinthemindset.Ononehand,theasynchronousnatureofthereactorpatternrequiresadifferentprogrammingstylemadeofcallbacksandthingsthathappenatalatertime,withoutworryingtoomuchaboutthreadsandraceconditions.Ontheotherhand,themodulepatternanditsprinciplesofsimplicityandminimalismcreatesinterestingnewscenariosintermsofreusability,maintenance,andusability.
Finally,besidestheobvioustechnicaladvantagesofbeingfast,efficient,andbasedonJavaScript,Node.jsisattractingsomuchinterestbecauseoftheprincipleswehavejustdiscovered.Formany,graspingtheessenceofthisworldfeelslikereturningtotheorigins,toamorehumanewayofprogrammingforbothsizeandcomplexityandthat'swhydevelopersendupfallinginlovewithNode.js.
Inthenextchapter,wewillfocusourattentiononthemechanismstohandleasynchronouscode,wewillseehowcallbackscaneasilybecomeourenemy,andwewilllearnhowtofixthatbyusingsomesimpleprinciples,patterns,orevenconstructsthatdonotrequireacontinuation-passingstyleprogramming.
Chapter2.AsynchronousControlFlowPatternsMovingfromasynchronousprogrammingstyletoaplatformsuchasNode.js,wherecontinuation-passingstyleandasynchronousAPIsarethenorm,canbefrustrating.Writingasynchronouscodecanbeadifferentexperience,especiallywhenitcomestocontrolflow.Simpleproblemssuchasiteratingoverasetoffiles,executingtasksinsequence,orwaitingforasetofoperationstocomplete,requirethedevelopertotakenewapproachesandtechniquestoavoidendingupwritinginefficientandunreadablecode.Onecommonmistakeistofallintothetrapofthecallbackhellproblemandseethecodegrowinghorizontallyratherthanvertically,withanestingthatmakesevensimpleroutineshardtoreadandmaintain.
Inthischapter,wewillseehowit'sactuallypossibletotamecallbacksandwriteclean,manageableasynchronouscodebyusingsomedisciplineandwiththeaidofsomepatterns.Wewillseehowcontrolflowlibraries,suchasasync,cansignificantlysimplifyourproblems,andwewillalsodiscoverthatthecontinuation-passingstyleisnottheonlywaytoimplementasynchronousAPI.Infact,wewilllearnhowPromisesandECMAScript6generatorscanbepowerfulandflexiblealternatives.Foreachoneoftheseparadigms,wewilllearnaboutpatternsthatwillhelpusimplementthemostcommoncontrolflows,andbytheendofthechapter,weshouldbereadyandconfidenttowritecleanandefficientasynchronouscode.
ThedifficultiesofasynchronousprogrammingLosingcontrolofasynchronouscodeinJavaScriptisundoubtedlyeasy.Closuresandin-placedefinitionofanonymousfunctionsallowasmoothprogrammingexperiencethatdoesn'trequirethedevelopertojumptootherpointsinthecodebase.ThisisperfectlyinlinewiththeKISSprinciple;it'ssimple,itkeepsthecodeflowing,andwegetitworkinginlesstime.Unfortunately,sacrificingqualitiessuchasmodularity,reusability,andmaintainabilitywillsoonerorlaterleadtotheuncontrolledproliferationofcallbacknesting,thegrowthinthesizeoffunctions,andwillleadtopoorcodeorganization.Mostofthetime,creatingclosuresisnotfunctionallyneeded,soit'smoreamatterofdisciplinethanaproblemrelatedtoasynchronousprogramming.Recognizingthatourcodeisbecomingunwieldy—orevenbetter,knowinginadvancethatitmightbecomeunwieldy—andthenactingaccordinglywiththemostadequatesolutioniswhatdifferentiatesanovicefromanexpert.
CreatingasimplewebspiderToexplaintheproblem,wewillcreatealittlewebspider,acommand-lineapplicationthattakesinawebURLasinputanddownloadsitscontentslocallyintoafile.Inthecodepresentedinthischapter,wearegoingtouseafewnpmdependencies:
request:AlibrarytostreamlineHTTPcallsmkdirp:Asmallutilitytocreatedirectoriesrecursively
Also,wewilloftenrefertoalocalmodulenamed./utilities,whichcontainssomehelperswhichwewillbeusinginourapplication.Weomitthecontentsofthisfileforbrevity,butyoucanfindthefullimplementation,alongwithapackage.jsoncontainingthefulllistofdependencies,inthedownloadpackforthisbookavailableathttp://www.packtpub.com.
Thecorefunctionalityofourapplicationiscontainedinsideamodulenamedspider.js.Let'sseehowitlooks.Tostartwith,let'sloadallthedependenciesthatwearegoingtouse:
varrequest=require('request');
varfs=require('fs');
varmkdirp=require('mkdirp');
varpath=require('path');
varutilities=require('./utilities');
Next,wecreateanewfunctionnamedspider(),whichtakesintheURLtodownloadandacallbackfunctionthatwillbeinvokedwhenthedownloadprocesscompletes:
functionspider(url,callback){
varfilename=utilities.urlToFilename(url);
fs.exists(filename,function(exists){//[1]
if(!exists){
console.log("Downloading"+url);
request(url,function(err,response,body){//[2]
if(err){
callback(err);
}else{
mkdirp(path.dirname(filename),function(err){
//[3]
if(err){
callback(err);
}else{
fs.writeFile(filename,body,function(err){
//[4]
if(err){
callback(err);
}else{
callback(null,filename,true);
}
});
}
});
}
});
}else{
callback(null,filename,false);
}
});
}
Theprecedingfunctionexecutesthefollowingtasks:
1. ChecksiftheURLwasalreadydownloadedbyverifyingthatthecorrespondingfilewasnotalreadycreated:
fs.exists(filecodename,function(exists)…
2. Ifthefileisnotfound,theURLisdownloadedusingthefollowinglineofcode:
request(url,function(err,response,body)…
3. Then,wemakesurewhetherthedirectorythatwillcontainthefileexistsornot:
mkdirp(path.dirname(filename),function(err)…
4. Finally,wewritethebodyoftheHTTPresponsetothefilesystem:
fs.writeFile(filename,body,function(err)…
Tocompleteourwebspiderapplication,wejustneedtoinvokethespider()functionbyprovidingaURLasaninput(inourcase,wereaditfromthecommand-linearguments):
spider(process.argv[2],function(err,filename,downloaded){
if(err){
console.log(err);
}elseif(downloaded){
console.log('Completedthedownloadof"'+filename+'"');
}else{
console.log('"'+filename+'"wasalreadydownloaded');
}
});
Now,wearereadytotryourwebspiderapplication,butfirst,makesureyouhavetheutilities.jsmoduleandthepackage.jsoncontainingthefulllistofdependenciesinyourprojectdirectory.Then,installallthedependenciesbyrunningthefollowingcommand:
npminstall
Next,wecanexecutethespidermoduletodownloadthecontentsofawebpage,withacommandlikethis:
nodespiderhttp://www.example.com
NoteOurwebspiderapplicationrequiresthatwealwaysincludetheprotocol(forexample,http://)intheURLweprovide.Also,donotexpectHTMLlinkstoberewrittenorresourcessuchasimagestobedownloadedasthisisjustasimpleexampletodemonstratehowasynchronousprogrammingworks.
ThecallbackhellLookingatthespider()functionwedefinedearlier,wecansurelynoticethateventhoughthealgorithmweimplementedisreallystraightforward,theresultingcodehasseverallevelsofindentationandisveryhardtoread.ImplementingasimilarfunctionwithdirectstyleblockingAPIwouldbestraightforward,andtherewouldbeveryfewchancestomakeitlooksowrong.However,usingasynchronousCPSisanotherstory,andmakingbaduseofclosurescanleadtoanincrediblybadcode.
Thesituationwheretheabundanceofclosuresandin-placecallbackdefinitionstransformthecodeintoanunreadableandunmanageableblobisknownascallbackhell.It'soneofthemostwellrecognizedandsevereanti-patternsinNode.jsandJavaScriptingeneral.Thetypicalstructureofacodeaffectedbythisproblemlookslikethefollowing:
asyncFoo(function(err){
asyncBar(function(err){
asyncFooBar(function(err){
[...]
});
});
});
Wecanseehowcodewritteninthiswayassumestheshapeofapyramidduetothedeepnestingandthat'swhyitisalsocolloquiallyknownasthepyramidofdoom.
Themostevidentproblemwithcodesuchastheprecedingoneisthepoorreadability.Duetothenestingbeingtoodeep,it'salmostimpossibletokeeptrackof
whereafunctionendsandwhereanotheronebegins.
Anotherissueiscausedbytheoverlappingofthevariablenamesusedineachscope.Often,wehavetousesimilarorevenidenticalnamestodescribethecontentofavariable.Thebestexampleistheerrorargumentreceivedbyeachcallback.Somepeopleoftentrytousevariationsofthesamenametodifferentiatetheobjectineachscope—forexample,err,error,err1,err2,andsoon;othersprefertojusthidethevariabledefinedinthescopebyalwaysusingthesamename;forexample,err.Boththealternativesarefarfromperfect,andcauseconfusionandincreasetheprobabilityofintroducingdefects.
Also,wehavetokeepinmindthatclosurescomeatasmallpriceintermsofperformancesandmemoryconsumption.Inaddition,theycancreatememoryleaksthatarenotsoeasytoidentifybecauseweshouldn'tforgetthatanycontextreferencedbyanactiveclosureisretainedfromgarbagecollection.
NoteForagreatintroductiontohowclosuresworkinV8youcanrefertotheblogpostbyVyacheslavEgorov,asoftwareengineeratGoogleworkingonV8,athttp://mrale.ph/blog/2012/09/23/grokking-v8-closures-for-fun.html.
Ifwelookatourspider()function,wewillnoticethatitclearlyrepresentsacallbackhellsituationandhasalltheproblemswejustdescribed.That'sexactlywhatwearegoingtofixwiththepatternsandtechniqueswewilllearninthischapter.
UsingplainJavaScriptNowthatwehavemetourfirstexampleofcallbackhell,weknowwhatweshoulddefinitelyavoid;however,that'snottheonlyconcernwhenwritingasynchronouscode.Infact,thereareseveralsituationswherecontrollingtheflowofasetofasynchronoustasksrequirestheuseofspecificpatternsandtechniques,especiallyifweareusingonlyplainJavaScriptwithouttheaidofanyexternallibrary.Forexample,iteratingoveracollectionbyapplyinganasynchronousoperationinsequenceisnotaseasyasinvokingforEach()overanarray,butitactuallyrequiresatechniquesimilartoarecursion.
Inthissection,wewilllearnnotonlyabouthowtoavoidthecallbackhellbutalsoabouthowtoimplementsomeofthemostcommoncontrolflowpatternsusingonlysimpleandplainJavaScript.
CallbackdisciplineWhenwritingasynchronouscode,thefirstruletokeepinmindistonotabuseclosureswhendefiningcallbacks.Itcanbetemptingtodoso,becauseitdoesnotrequireanyadditionalthinkingforproblemssuchasmodularizationandreusability;however,wehaveseenhowthiscanhavemoredisadvantagesthanadvantages.Mostofthetimes,fixingthecallbackhellproblemdoesnotrequireanylibrary,fancytechnique,orchangeofparadigmbutjustsomecommonsense.
Thesearesomebasicprinciplesthatcanhelpuskeepthenestinglevellowandimprovetheorganizationofourcodeingeneral:
Youmustexitassoonaspossible.Usereturn,continue,orbreak,dependingonthecontext,toimmediatelyexitthecurrentstatementinsteadofwriting(andnesting)completeif/elsestatements.Thiswillhelpkeepourcodeshallow.Youneedtocreatenamedfunctionsforcallbacks,keepingthemoutofclosuresandpassingintermediateresultsasarguments.Namingourfunctionswillalsomakethemlookbetterinstacktraces.Youneedtomodularizethecode.Splitthecodeintosmaller,reusablefunctionswheneverit'spossible.
ApplyingthecallbackdisciplineTodemonstratethepoweroftheearliermentionedprinciples,let'sapplythemtofixthecallbackhellprobleminourwebspiderapplication.
Forthefirststep,wecanrefactorourerror-checkingpatternbyremovingtheelsestatement.Thisismadepossiblebyreturningfromthefunctionimmediatelyafterwereceiveanerror.So,insteadofhavingacodesuchasthefollowing:
if(err){
callback(err);
}else{
//codetoexecutewhentherearenoerrors
}
Wecanimprovetheorganizationofourcodebywritingthefollowingoneinstead:
if(err){
returncallback(err);
}
//codetoexecutewhentherearenoerrors
Withthissimpletrick,weimmediatelyhaveareductionofthenestinglevelofourfunctions;itiseasyanddoesn'trequireanycomplexrefactoring.
NoteAcommonmistakewhenexecutingtheoptimizationwejustdescribed,isforgettingtoterminatethefunctionafterthecallbackisinvoked.Fortheerror-handlingscenario,thefollowingcodeisatypicalsourceofdefects:
if(err){
callback(err);
}
//codetoexecutewhentherearenoerrors
Weshouldneverforgetthattheexecutionofourfunctionwillcontinueevenafterweinvokethecallback.Itisthenimportanttoinsertareturninstructiontoblocktheexecutionoftherestofthefunction.Alsonotethatitdoesn'treallymatterwhatoutputisreturnedbythefunction;therealresult(orerror)isproducedasynchronouslyandpassedtothecallback.Thereturnvalueoftheasynchronousfunctionisusuallyignored.Thispropertyallowsustowriteshortcutssuchasthefollowing:
returncallback(...)
Insteadoftheslightlymoreverboseonessuchasthefollowing:
callback(...)
return;
Asasecondoptimizationforourspider()function,wecantrytoidentifyreusablepiecesofcode.Forexample,thefunctionalitythatwritesagivenstringtoafilecanbeeasilyfactoredoutintoaseparatefunctionasfollows:
functionsaveFile(filename,contents,callback){
mkdirp(path.dirname(filename),function(err){
if(err){
returncallback(err);
}
fs.writeFile(filename,contents,callback);
});
}
Followingthesameprinciple,wecancreateagenericfunctionnameddownload()whichtakesaURLandafilenameasinput,anddownloadstheURLintothegivenfile.Internally,wecanusethesaveFile()functionwecreatedearlier.
functiondownload(url,filename,callback){
console.log('Downloading'+url);
request(url,function(err,response,body){
if(err){
returncallback(err);
}
saveFile(filename,body,function(err){
console.log('Downloadedandsaved:'+url);
if(err){
returncallback(err);
}
callback(null,body);
});
});
}
Forthelaststep,wemodifythespider()function,which,thankstoourchanges,willnowlooklikethefollowing:
functionspider(url,callback){
varfilename=utilities.urlToFilename(url);
fs.exists(filename,function(exists){
if(exists){
returncallback(null,filename,false);
}
download(url,filename,function(err){
if(err){
returncallback(err);
}
callback(null,filename,true);
})
});
}
Thefunctionalityandtheinterfaceofthespider()functionremainexactlythesame;whatchangedisonlythewaythecodewasorganized.Byapplyingthebasicprinciplesthatwediscussed,wewereabletodrasticallyreducethenestingofourcodeandatthesametimeincreaseitsreusabilityandtestability.Infact,wecouldthinkofexportingbothsaveFile()anddownload(),sothatwecanreusetheminothermodules.Thisalsoallowsustotesttheirfunctionalitymoreeasily.
Therefactoringwecarriedoutinthissectionclearlydemonstratesthatmostofthetime,allweneedisjustsomedisciplinetomakesurewedonotabuseclosuresandanonymousfunctions.Itworksbrilliantly,requiresminimaleffort,andjustusesplainJavaScript.
SequentialexecutionWenowbeginourexplorationoftheasynchronouscontrolflowpatterns.Wewillstartbyanalyzingthesequentialexecutionflow.
Executingasetoftasksinsequencemeansrunningthemoneatatime,oneaftertheother.Theorderofexecutionmattersandmustbepreserved,becausetheresultofataskinthelistmayaffecttheexecutionofthenext.Thefollowingimageillustratesthisconcept:
Therearedifferentvariationsofthisflow:
Executingasetofknowntasksinsequence,withoutchainingorpropagatingresultsUsingtheoutputofataskastheinputforthenext(alsoknownaschain,pipeline,orwaterfall)Iteratingoveracollectionwhilerunninganasynchronoustaskoneachelement,oneaftertheother
Sequentialexecution,despitebeingtrivialwhenimplementedusingthedirectstyleblockingAPI,isusuallythemaincauseofthecallbackhellproblemwhenusing
asynchronousCPS.
ExecutingaknownsetoftasksinsequenceWealreadymetasequentialexecutionwhileimplementingthespider()functionintheprevioussection.Byapplyingthesimplerulesthatwestudied,wewereabletoorganizeasetofknowntasksinasequentialexecutionflow.Takingthatcodeasaguideline,wecanthengeneralizethesolutionwiththefollowingpattern:
functiontask1(callback){
asyncOperation(function(){
task2(callback);
});
}
functiontask2(callback){
asyncOperation(function(result){
task3(callback);
});
}
functiontask3(callback){
asyncOperation(function(){
callback();
});
}
task1(function(){
//task1,task2,task3completed
});
Theprecedingpatternshowshoweachtaskinvokesthenextuponthecompletionofagenericasynchronousoperation.Thepatternputstheemphasisonthemodularizationoftasks,showinghowclosuresarenotalwaysnecessarytohandleasynchronouscode.
SequentialiterationThepatternwedescribedearlierworksperfectlyifweknowinadvancewhatandhowmanytasksaretobeexecuted.Thisallowsustohardcodetheinvocationofthenexttaskinthesequence;butwhathappensifwewanttoexecuteanasynchronousoperationforeachiteminacollection?Incasessuchasthis,wecannothardcodethetasksequenceanymore,instead,wehavetobuilditdynamically.
Webspiderversion2
Toshowanexampleofsequentialiteration,let'sintroduceanewfeaturetotheweb
spiderapplication.Wenowwanttodownloadallthelinkscontainedinawebpagerecursively.Todothat,wearegoingtoextractallthelinksfromthepageandthentriggerourwebspideroneachoneofthemrecursivelyandinsequence.
Thefirststepismodifyingourspider()functionsothatittriggersarecursivedownloadofallthelinksofapagebyusingafunctionnamedspiderLinks(),whichwearegoingtocreateshortly.
Also,insteadofcheckingifthefilealreadyexists,wenowtrytoreadit,andstartspideringitslinks;thisway,weareabletoresumeinterrupteddownloads.Asafinalchange,wemakesurewepropagateanewparameter,nesting,whichhelpsuslimittherecursiondepth.Theresultantcodeisasfollows:
functionspider(url,nesting,callback){
varfilename=utilities.urlToFilename(url);
fs.readFile(filename,'utf8',function(err,body){
if(err){
if(err.code!=='ENOENT'){
returncallback(err);
}
returndownload(url,filename,function(err,body){
if(err){
returncallback(err);
}
spiderLinks(url,body,nesting,callback);
});
}
spiderLinks(url,body,nesting,callback);
});
}
Sequentialcrawlingoflinks
Nowwecancreatethecoreofthisnewversionofourwebspiderapplication,thespiderLinks()function,whichdownloadsallthelinksofanHTMLpageusingasequentialasynchronousiterationalgorithm.Payattentiontothewaywearegoingtodefinethatinthefollowingcodeblock:
functionspiderLinks(currentUrl,body,nesting,callback){
if(nesting===0){
returnprocess.nextTick(callback);
}
varlinks=utilities.getPageLinks(currentUrl,body);//[1]
functioniterate(index){//[2]
if(index===links.length){
returncallback();
}
spider(links[index],nesting-1,function(err){//[3]
if(err){
returncallback(err);
}
iterate(index+1);
});
}
iterate(0);//[4]
}
Theimportantstepstounderstandfromthisnewfunctionareasfollows:
1. Weobtainthelistofallthelinkscontainedinthepageusingtheutilities.getPageLinks()function.Thisfunctionreturnsonlythelinkspointingtoaninternaldestination(samehostname)
2. Weiterateoverthelinksusingalocalfunctioncallediterate(),whichtakestheindexofthenextlinktoanalyze.Inthisfunction,thefirstthingwedoischeckingiftheindexisequaltothelengthofthelinksarray,inwhichcaseweimmediatelyinvokethecallback()function,asitmeansweprocessedalltheitems.
3. Atthispoint,everythingshouldbereadyforprocessingthelink.Weinvokethespider()functionbydecreasingthenestinglevelandinvokingthenextstepoftheiterationwhentheoperationcompletes.
4. AsthelaststepinthespiderLinks()function,webootstraptheiterationbyinvokingiterate(0).
Thealgorithmwejustpresentedallowsustoiterateoveranarraybyexecutinganasynchronousoperationinsequence,whichinourcaseisthespider()function.
Wecannowtrythisnewversionofthespiderapplicationandwatchitdownloadallthelinksofawebpagerecursively,oneaftertheother.Tointerrupttheprocess,whichcantakeawhileiftherearemanylinks,rememberthatwecanalwaysuseCtrl+C.Ifwethendecidetoresumeit,wecandosobylaunchingthespiderapplicationandprovidingthesameURLweusedforthefirstrun.
NoteNowthatourwebspiderapplicationmightpotentiallytriggerthedownloadofanentirewebsite,pleaseconsiderusingitcarefully.Forexample,donotsetahighnestinglevelorleavethespiderrunningformorethanafewseconds.Itisnotpolitetooverloadaserverwiththousandsofrequests.Insomecircumstancesthiscanalsobeconsideredillegal.Doitresponsibly!
Thepattern
ThecodeofthespiderLinks()functionthatweshowedpreviouslyisaclearexampleofhowit'spossibletoiterateoveracollectionwhileapplyinganasynchronousoperation.Wecanalsonoticethatit'sapatternthatcanbeadaptedtoanyothersituationwherewehavetheneedtoiterateasynchronouslyinsequenceovertheelementsofacollectionoringeneraloveralistoftasks.Thispatterncanbegeneralizedasfollows:
functioniterate(index){
if(index===tasks.length){
returnfinish();
}
vartask=tasks[index];
task(function(){
iterate(index+1);
});
}
functionfinish(){
//iterationcompleted
}
iterate(0);
NoteIt'simportanttonoticethatthesetypesofalgorithmsbecomereallyrecursiveiftask()isasynchronousoperation.Insuchacase,thestackwillnotunwindateverycycleandtheremightbeariskofhittingthemaximumcallstacksizelimit.
Thepatternwejustpresentedisverypowerfulasitcanadapttoseveralsituations;forexample,wecanmapthevaluesofanarrayorwecanpasstheresultsofanoperationtothenextoneintheiterationtoimplementareducealgorithm,wecanquittheloopprematurelyifaparticularconditionismet,orwecaneveniterateoveraninfinitenumberofelements.
Wecouldalsochoosetogeneralizethesolutionevenfurtherbywrappingitintoafunctionhavingasignaturesuchasthefollowing:
iterateSeries(collection,iteratorCallback,finalCallback)
Weleavethistoyouasanexercise.
NotePattern(sequentialiterator):executealistoftasksinsequencebycreatingafunctionnamediterator,whichinvokesthenextavailabletaskinthecollectionandmakessuretoinvokethenextstepoftheiterationwhenthecurrenttaskcompletes.
ParallelexecutionTherearesomesituationswheretheorderoftheexecutionofasetofasynchronoustasksisnotimportantandallwewantisjusttobenotifiedwhenallthoserunningtasksarecompleted.Suchsituationsarebetterhandledusingaparallelexecutionflow,asshowninthefollowingdiagram:
ThismaysoundstrangeifweconsiderthatNode.jsissinglethreaded,butifwerememberwhatwediscussedinChapter1,Node.jsDesignFundamentals,werealizethateventhoughwehavejustonethread,wecanstillachieveconcurrency,thankstothenonblockingnatureofNode.js.Infact,thewordparallelisusedimproperlyinthiscase,asitdoesnotmeanthatthetasksrunsimultaneously,butratherthattheirexecutioniscarriedoutbyanunderlyingnonblockingAPIandinterleavedbytheeventloop.
Asweknow,ataskgivesthecontrolbacktotheeventloopwhenitrequestsanewasynchronousoperationallowingtheeventlooptoexecuteanothertask.Theproperwordtouseforthiskindofflowisconcurrency,butwewillstilluseparallelforsimplicity.
ThefollowingdiagramshowshowtwoasynchronoustaskscanruninparallelinaNode.jsprogram:
Inthepreviousimage,wehaveaMainfunctionthatexecutestwoasynchronoustasks:
1. TheMainfunctiontriggerstheexecutionofTask1andTask2.Asthesetriggeranasynchronousoperation,theyimmediatelyreturnthecontrolbacktotheMainfunction,whichthenreturnsittotheeventloop.
2. WhentheasynchronousoperationofTask1iscompleted,theeventloopgivescontroltoit.WhenTask1completesitsinternalsynchronousprocessingaswell,itnotifiestheMainfunction.
3. WhentheasynchronousoperationtriggeredbyTask2iscompleted,theeventloopinvokesitscallback,givingthecontrolbacktoTask2.AttheendofTask2,theMainfunctionisagainnotified.Atthispoint,theMainfunctionknowsthatbothTask1andTask2arecomplete,soitcancontinueitsexecutionorreturntheresultsoftheoperationstoanothercallback.
Inshort,thismeansthatinNode.js,wecanexecuteinparallelonlyasynchronousoperations,becausetheirconcurrencyishandledinternallybythenonblockingAPIs.InNode.js,synchronous(blocking)operationscannotrunconcurrentlyunlesstheirexecutionisinterleavedwithanasynchronousoperation,ordeferredwith
setTimeout()orsetImmediate().WewillseethisinmoredetailinChapter6,Recipes.
Webspiderversion3Ourwebspiderapplicationseemslikeaperfectcandidatetoapplytheconceptofparallelexecution.Sofar,ourapplicationisexecutingtherecursivedownloadofthelinkedpagesinasequentialfashion.Wecaneasilyimprovetheperformanceofthisprocessbydownloadingallthelinkedpagesinparallel.
Todothat,wejustneedtomodifythespiderLinks()functiontomakesuretospawnallthespider()tasksatonce,andtheninvokethefinalcallbackonlywhenallofthemhavecompletedtheirexecution.Solet'smodifyourspiderLinks()functionasfollows:
functionspiderLinks(currentUrl,body,nesting,callback){
if(nesting===0){
returnprocess.nextTick(callback);
}
varlinks=utilities.getPageLinks(currentUrl,body);
if(links.length===0){
returnprocess.nextTick(callback);
}
varcompleted=0,errored=false;
functiondone(err){
if(err){
errored=true;
returncallback(err);
}
if(++completed===links.length&&!errored){
returncallback();
}
}
links.forEach(function(link){
spider(link,nesting-1,done);
});
}
Let'sexplainwhatwechanged.Aswementionedearlier,thespider()tasksarenowstartedallatonce.Thisispossiblebysimplyiteratingoverthelinksarrayandstartingeachtaskwithoutwaitingforthepreviousonetofinish:
links.forEach(function(link){
spider(link,nesting-1,done);
});
Then,thetricktomakeourapplicationwaitforallthetaskstocompleteistoprovidethespider()functionwithaspecialcallback,whichwecalldone().Thedone()functionincreasesacounterwhenaspidertaskcompletes.Whenthenumberofcompleteddownloadsreachesthesizeofthelinksarray,thefinalcallbackisinvoked:
functiondone(err){
if(err){
errored=true;
returncallback(err);
}
if(++completed===links.length&&!errored){
callback();
}
}
Withthesechangesinplace,ifwenowtrytorunourspideragainstawebpage,wewillnoticeahugeimprovementinthespeedoftheoverallprocess,aseverydownloadiscarriedoutinparallelwithoutwaitingforthepreviouslinktobeprocessed.
ThepatternAlso,fortheparallelexecutionflow,wecanextractournicelittlepattern,whichwecanadaptandreusefordifferentsituations.Wecanrepresentagenericversionofthepatternwiththefollowingcode:
vartasks=[...];
varcompleted=0;
tasks.forEach(function(task){
task(function(){
if(++completed===tasks.length){
finish();
}
});
});
functionfinish(){
//allthetaskscompleted
}
Withsmallmodifications,wecanadaptthepatterntoaccumulatetheresultsofeachtaskintoacollection,tofilterormaptheelementsofanarray,ortoinvokethefinish()callbackassoonasoneoragivennumberoftaskscomplete(thislastsituationinparticulariscalledcompetitiverace).
NotePattern(unlimitedparallelexecution):runasetofasynchronoustasksinparallelbyspawningthemallatonce,andthenwaitforallofthemtocompletebycountingthenumberoftimestheircallbacksareinvoked.
FixingraceconditionsinthepresenceofconcurrenttasksRunningasetoftasksinparallelcanbeapainwhenusingblockingI/Oincombinationwithmultiplethreads.However,wehavejustseenthatinNode.jsthisisatotallydifferentstory;runningmultipleasynchronoustasksinparallelisinfactstraightforwardandcheapintermsofresources.ThisisoneofthemostimportantstrengthsforNode.js,becauseitmakesparallelizationacommonpracticeratherthanacomplextechniquetouseonlywhenstrictlynecessary.
AnotherimportantcharacteristicoftheconcurrencymodelofNode.jsisthewaywedealwithtasksynchronizationandraceconditions.Inmultithreadedprogramming,thisisusuallydoneusingconstructssuchaslocks,mutexes,semaphores,andmonitors,anditcanbeoneofthemostcomplexaspectsofparallelizationwhichhasconsiderableimpactonperformancesaswell.InNode.js,weusuallydon'tneedanyfancysynchronizationmechanism,aseverythingrunsonasinglethread!However,thisdoesn'tmeanthatwecan'thaveraceconditions,onthecontrary,theycanbequitecommon.Therootoftheproblemisthedelaybetweentheinvocationofanasynchronousoperationandthenotificationofitsresult.Tomakeaconcreteexample,wecanreferagaintoourwebspiderapplication,inparticular,thelastversionwecreated,whichactuallycontainsaracecondition(canyouspotit?).
Theproblemwearetalkingaboutliesinthespider()function,wherewecheckifafilealreadyexists,beforestartingtodownloadthecorrespondingURL:
functionspider(url,nesting,callback){
varfilename=utilities.urlToFilename(url);
fs.readFile(filename,'utf8',function(err,body){
if(err){
if(err.code!=='ENOENT'){
returncallback(err);
}
returndownload(url,filename,function(err,body){
[...]
Theproblemisthat,twospidertasksoperatingonthesameURLmightinvokefs.readFile()onthesamefilebeforeoneofthetwotaskscompletesthedownloadandcreatesafile,causingbothtaskstostartadownload.Thissituationisshowninthefollowingdiagram:
TheprecedingdiagramshowshowTask1andTask2areinterleavedinthesinglethreadofNode.jsandhowanasynchronousoperationcanactuallyintroducearacecondition.Inourcase,thetwospidertasksendupdownloadingthesamefile.
Howcanwefixthat?Theanswerismuchsimplerthanwemightthinkittobe.Infact,allweneedisavariabletomutuallyexcludemultiplespider()tasksrunningonthesameURL.Thiscanbeachievedwithsomecodesuchasthefollowing:
varspidering={};
functionspider(url,nesting,callback){
if(spidering[url]){
returnprocess.nextTick(callback);
}
spidering[url]=true;
[...]
Thefixdoesnotrequiremanycomments.Wesimplyexitthefunctionimmediatelyiftheflagforthegivenurlissetinthespideringmap;otherwise,wesettheflagandcontinuewiththedownload.Forourcase,wedon'tneedtoreleasethelock,aswearenotinterestedindownloadingaURLtwotimes,evenifthespidertasksareexecutedduringtwocompletelydifferentpointsintime.
Raceconditionscancausemanyproblems,evenifweareinasingle-threadedenvironment.Insomecircumstances,theycanleadtodatacorruptionandareusuallyveryhardtodebugbecauseoftheirephemeralnature.So,it'salwaysagoodpracticetodoublecheckforthistypeofsituationwhenrunningtasksinparallel.
LimitedparallelexecutionOften,spawningparalleltaskswithoutcontrolcanleadtoanexcessiveload.Imagine
havingthousandsoffilestoread,URLstoaccess,ordatabasequeriestoruninparallel.Acommonprobleminsuchsituationsisrunningoutofresources,forexample,byutilizingallthefiledescriptorsavailableforanapplicationwhentryingtoopentoomanyfilesatonce.Inawebapplication,itmayalsocreateavulnerabilitythatisexploitablewithDenialofService(DoS)attacks.Inallsuchsituations,itisagoodideatolimitthenumberoftasksthatcanrunatthesametime.Thisway,wecanaddsomepredictabilitytotheloadofourserverandalsomakesurethatourapplicationwillnotrunoutofresources.Thefollowingdiagramdescribesasituationwherewehavefivetasksthatruninparallelwithaconcurrencylimitof2:
Fromtheprecedingfigure,itshouldbeclearhowouralgorithmwillwork:
1. Initially,wespawnasmanytasksaswecanwithoutexceedingtheconcurrencylimit.
2. Then,everytimeataskiscompleted,wespawnoneormoretasksuntilwedon'treachthelimitagain.
LimitingtheconcurrencyWenowpresentapatterntoexecuteasetofgiventasksinparallelwithlimitedconcurrency:
vartasks=[...];
varconcurrency=2,running=0,completed=0,index=0;
functionnext(){//[1]
while(running<concurrency&&index<tasks.length){
task=tasks[index++];
task(function(){//[2]
if(completed===tasks.length){
returnfinish();
}
completed++,running--;
next();
});
running++;
}
}
next();
functionfinish(){
//alltasksfinished
}
Thisalgorithmcanbeconsideredamixbetweenasequentialexecutionandaparallelexecution.Infact,wemightnoticesimilaritieswithboththepatternswepresentedearlierinthechapter:
1. Wehaveaniteratorfunction,whichwecallednext(),andthenaninnerloopthatspawnsinparallelasmanytasksaspossiblewhilestayingwithintheconcurrencylimit.
2. Thenextimportantpartisthecallbackwepasstoeachtask,whichchecksifwecompletedallthetasksinthelist.Iftherearestilltaskstorun,itinvokesnext()tospawnanotherbunchoftasks.
Prettysimple,isn'tit?
GloballylimitingtheconcurrencyOurwebspiderapplicationisperfectforapplyingwhatwelearnedaboutlimitingtheconcurrencyofasetoftasks.Infact,toavoidthesituationinwhichwehavethousandsoflinkscrawledatthesametime,wecanenforcealimitontheconcurrencyofthisprocessbyaddingsomepredictabilityonthenumberofconcurrentdownloads.
NoteNode.jsversionsbefore0.11arealreadylimitingthenumberofconcurrentHTTPconnectionsperhostto5.Thiscan,however,bechangedtoaccommodateourneeds.Findoutmoreintheofficialdocsathttp://nodejs.org/docs/v0.10.0/api/http.html#http_agent_maxsockets.StartingfromNode.js0.11,thereisnodefaultlimitonthenumberofconcurrentconnections.
WecouldapplythepatternwejustlearnedtoourspiderLinks()function,butwhatwewouldobtainisonlylimitingtheconcurrencyofasetoflinksfoundwithinonesinglepage.Ifwechose,forexample,aconcurrencyof2,wewouldhaveatmosttwolinksdownloadedinparallelforeachpage.However,aswecandownloadmultiplelinksatonce,eachpagewouldthenspawnanothertwodownloads,causingthegrandtotalofdownloadoperationstogrowexponentiallyanyway.
Queuestotherescue
Whatwereallywantthen,istolimittheglobalnumberofdownloadoperationswecanhaverunninginparallel.Wecouldslightlymodifythepatternshowedbefore,butweprefertoleavethisasanexerciseforyou,aswewanttotakethisopportunitytointroduceanothermechanism,whichmakesuseofqueues,tolimittheconcurrencyofmultipletasks.Let'sseehowthisworks.
WearenowgoingtoimplementasimpleclassnamedTaskQueue,whichwillcombineaqueuewiththealgorithmwepresentedbefore.Let'screateanewmodulenamedtaskQueue.js,andlet'sstartbydefiningitsconstructor:
functionTaskQueue(concurrency){
this.concurrency=concurrency;
this.running=0;
this.queue=[];
}
Theconstructortakesasinputonlytheconcurrencylimit,butbesidesthat,itinitializesotherinstancevariablesthatwearegoingtoneedlater.
Next,weimplementthepushTask()methodasfollows:
TaskQueue.prototype.pushTask=function(task,callback){
this.queue.push(task);
this.next();
}
Thepreviousfunctionsimplyaddsanewtasktothequeueandthenbootstrapstheexecutionoftheworkerbyinvokingthis.next().
Let'sseehowthenext()methodlooks;itsroleistospawnasetoftasksfromthequeueensuringthatitdoesnotexceedtheconcurrencylimit:
TaskQueue.prototype.next=function(){
varself=this;
while(self.running<self.concurrency&&self.queue.length)
{
vartask=self.queue.shift();
task(function(err){
self.running--;
self.next();
});
self.running++;
}
}
Wemightnoticethatthismethodhassomesimilaritieswiththepatternthatlimitstheconcurrencywepresentedearlier.Itessentiallystartsasmanytasksfromthequeueaspossible,withoutexceedingtheconcurrencylimit.Wheneachtaskiscomplete,itupdatesthecountofrunningtasksandthenstartsanotherroundoftasksbyinvokingnext()again.TheinterestingpropertyoftheTaskQueueclassisthatitallowsustodynamicallyaddnewitemstothequeue.Theotheradvantageisthatnowwehaveacentralentityresponsibleforthelimitationoftheconcurrencyofourtasks,whichcanbesharedacrossalltheinstancesofafunction'sexecution.Inourcase,it'sthespider()function,aswewillseeinamoment.
Webspiderversion4
Nowthatwehaveourgenericqueuetoexecutetasksinalimitedparallelflow,let'suseitstraightawayinourwebspiderapplication.Let'sfirstloadthenewdependencyandcreateanewinstanceoftheTaskQueueclass,bysettingtheconcurrencylimitto2:
varTaskQueue=require('./taskQueue');
vardownloadQueue=newTaskQueue(2);
Next,weneedtoupdatethespiderLinks()functionsothatitcanusethenewlycreateddownloadQueue:
functionspiderLinks(currentUrl,body,nesting,callback){
if(nesting===0){
returnprocess.nextTick(callback);
}
varlinks=utilities.getPageLinks(currentUrl,body);
if(links.length===0){
returnprocess.nextTick(callback);
}
varcompleted=0,errored=false;
links.forEach(function(link){
downloadQueue.pushTask(function(done){
spider(link,nesting-1,function(err){
if(err){
errored=true;
returncallback(err);
}
if(++completed===links.length&&!errored){
callback();
}
done();
});
});
});
}
Thisnewimplementationofthefunctionisextremelyeasy,andit'sverysimilartothealgorithmforunlimitedparallelexecution,whichwepresentedearlierinthechapter.ThisisbecausewearedelegatingtheconcurrencycontroltotheTaskQueueobject,andtheonlythingwehavetodoistocheckwhenallourtasksarecomplete.Theonlyinterestingpartintheprecedingcodeishowwedefinedourtasks.
Werunthespider()functionbyprovidingacustomcallback.Inthecallback,wecheckifallthetasksrelativetothisexecutionofthespiderLinks()functionarecompleted.Whenthisconditionistrue,weinvokethefinalcallbackofthespiderLinks()function.Attheendofourtask,weinvokethedone()callbacksothatthequeuecancontinueitsexecution.
Afterwehaveappliedthesesmallchanges,wecannowtrytorunthespidermoduleagain.Thistime,weshouldnoticethatnomorethantwodownloadswillbeactiveatthesametime.
TheasynclibraryIfwetakealookforamomentateverycontrolflowpatternwehaveanalyzedsofar,wecanseethattheycouldbeusedasabasetobuildreusableandmoregenericsolutions.Forexample,wecouldwraptheunlimitedparallelexecutionalgorithmintoafunctionwhichacceptsalistoftasks,runstheminparallel,andinvokesthegivencallbackwhenallofthemarecomplete.Thiswayofwrappingcontrolflowalgorithmsintoreusablefunctionscanleadtoamoredeclarativeandexpressivewaytodefineasynchronouscontrolflows,andthat'sexactlywhatasync(https://npmjs.org/package/async)does.Theasynclibraryisaverypopularsolution,inNode.jsandJavaScriptingeneral,todealwithasynchronouscode.Itoffersasetoffunctionsthatgreatlysimplifytheexecutionofasetoftasksindifferentconfigurationsanditalsoprovidesusefulhelpersfordealingwithcollectionsasynchronously.Eventhoughthereareseveralotherlibrarieswithasimilargoal,asyncisadefactostandardinNode.jsduetoitspopularity.
Let'stryitstraightawaytodemonstrateitscapabilities.
SequentialexecutionTheasynclibrarycanhelpusimmenselywhenimplementingcomplexasynchronouscontrolflows,butonedifficultywithitischoosingtherighthelperfortheproblemathand.Forexample,forthecaseofthesequentialexecutionflow,therearearound20differentfunctionstochoosefrom,including:eachSeries(),mapSeries(),filterSeries(),rejectSeries(),reduce(),reduceRight(),detectSeries(),concatSeries(),series(),whilst(),doWhilst(),until(),doUntil(),forever(),waterfall(),compose(),seq(),applyEachSeries(),iterator(),andtimesSeries().
Choosingtherightfunctionisanimportantstepinwritingmorecompactandreadablecode,butthisalsorequiressomeexperienceandpractice.Inourexamples,wearegoingtocoveronlyafewofthesesituations,buttheywillstillprovideasolidbasetounderstandandefficientlyusetherestofthelibrary.
Now,toshowinpracticehowasyncworks,wearegoingtoadaptourwebspiderapplication.Let'sstartdirectlywithversion2,theonethatdownloadsallthelinksrecursivelyinsequence.
However,firstlet'smakesureweinstalltheasynclibraryintoourcurrentproject:
npminstallasync
Thenweneedtoloadthenewdependencyfromthespider.jsmodule:
varasync=require('async');
SequentialexecutionofaknownsetoftasksLet'smodifythedownload()functionfirst.Aswehavealreadyseen,itexecutesthefollowingthreetasksinsequence:
1. DownloadthecontentsofaURL.2. Createanewdirectoryifitdoesn'texistyet.3. SavethecontentsoftheURLintoafile.
Theidealfunctiontousewiththisflowisdefinitelyasync.series(),whichhasthefollowingsignature:
async.series(tasks,[callback])
Ittakesalistoftasksandacallbackfunctionthatisinvokedwhenallthetaskshavebeencompleted.Eachtaskisjustafunctionthatacceptsacallbackfunction,whichmustbeinvokedwhenthetaskcompletesitsexecution:
functiontask(callback){}
ThenicethingaboutasyncisthatitusesthesamecallbackconventionsofNode.js,anditautomaticallyhandleserrorpropagation.So,ifanyofthetasksinvokeitscallbackwithanerror,asyncwillskiptheremainingtasksinthelistandjumpdirectlytothefinalcallback.
Withthisinmind,let'sseehowthedownload()functionwouldchangebyusingasync:
functiondownload(url,filename,callback){
console.log('Downloading'+url);
varbody;
async.series([
function(callback){//[1]
request(url,function(err,response,resBody){
if(err){
returncallback(err);
}
body=resBody;
callback();
});
},
mkdirp.bind(null,path.dirname(filename)),//[2]
function(callback){//[3]
fs.writeFile(filename,body,callback);
}
],function(err){//[4]
console.log('Downloadedandsaved:'+url);
if(err){
returncallback(err);
}
callback(null,body);
});
}
Ifwerememberthecallbackhellversionofthiscode,wewillsurelyappreciatethewayasyncallowsustoorganizeourtasks.Thereisnoneedtonestcallbacksanymore,aswejusthavetoprovideaflatlistoftasks,usuallyoneforeachasynchronousoperation,whichasyncwillthenexecuteinsequence.Thisishowwedefineeachtask:
1. ThefirsttaskinvolvesthedownloadoftheURL.Also,wesavetheresponsebodyintoaclosurevariable(body)sothatitcanbesharedwiththeothertasks.
2. Inthesecondtask,wewanttocreatethedirectorythatwillholdthedownloadedpage.Wedothisbyperformingapartialapplicationofthemkdirp()function,bindingthepathofthedirectorytobecreated.Thisway,wecansaveafewlinesofcodeandincreaseitsreadability.
3. Atlast,wewritethecontentsofthedownloadedURLtoafile.Inthiscase,wecouldnotperformapartialapplication(aswedidforthesecondtask),becausethevariable,body,isonlyavailableafterthefirsttaskintheseriescompletes.However,wecanstillsavesomelinesofcodebyexploitingtheautomaticerrormanagementofasyncbysimplypassingthecallbackofthetaskdirectlytothefs.writeFile()function.
4. Afterallthetasksarecomplete,thefinalcallbackofasync.series()isinvoked.Inourcase,wearesimplydoingsomeerrormanagementandthenreturningthebodyvariabletocallbackofthedownload()function.
Forthisspecificsituation,apossiblealternativetoasync.series()wouldbeasync.waterfall(),whichstillexecutesthetasksinsequencebutinaddition,italsoprovidestheoutputofeachtaskasinputtothenext.Inoursituation,wecouldusethisfeaturetopropagatethebodyvariableuntiltheendofoursequence.Asanexercise,youcantrytoimplementthesamefunctionusingthewaterfallflowandthentakealookatthedifferences.
SequentialiterationWealreadysawfromthepreviousparagraphhowwecanexecuteasetofknowntasksinsequence;weusedasync.series()todothat.Wecouldusethesame
functionalitytoimplementthespiderLinks()functionofourwebspiderversion2,howeverasyncoffersamoreappropriatehelperforthespecificsituationinwhichwehavetoiterateoveracollection;thishelperisasync.eachSeries().Let'suseitthentoreimplementourspiderLinks()function(version2,downloadinseries)asfollows:
functionspiderLinks(currentUrl,body,nesting,callback){
if(nesting===0){
returnprocess.nextTick(callback);
}
varlinks=utilities.getPageLinks(currentUrl,body);
if(links.length===0){
returnprocess.nextTick(callback);
}
async.eachSeries(links,function(link,callback){
spider(link,nesting-1,callback);
},callback);
}
Ifwecomparetheprecedingcode,whichusesasync,withthecodeofthesamefunctionimplementedwithplainJavaScriptpatterns,wewillnoticethebigadvantagethatasyncgivesusintermsofcodeorganizationandreadability.
ParallelexecutionTheasynclibrarydoesn'tlackfunctionstohandleparallelflows,amongthemwecanfindeach(),map(),filter(),reject(),detect(),some(),every(),concat(),parallel(),applyEach(),andtimes().Theyfollowthesamelogicofthefunctionswehavealreadyseenforthesequentialexecution,withthedifferencethatthetasksprovidedareexecutedinparallel.
Todemonstratethat,wecantrytoapplyoneofthesefunctionstoimplementversion3ofourwebspiderapplication,theoneperformingthedownloadsusinganunlimitedparallelflow.
IfwerememberthecodeweusedearliertoimplementthesequentialversionofthespiderLinks()function,adaptingittomakeitworkinparallelisatrivialtask:
functionspiderLinks(currentUrl,body,nesting,callback){
[...]
async.each(links,function(link,callback){
spider(link,nesting-1,callback);
},callback);
}
Thefunctionisexactlythesameonethatweusedforthesequentialdownload,butthistimeweusedasync.each()insteadofasync.eachSeries().Thisclearlydemonstratesthepowerofabstractingtheasynchronousflowwithalibrarysuchasasync.Thecodeisnotboundtoaparticularexecutionflowanymore;thereisnocodespecificallywrittenforthat,mostofitisjustapplicationlogic.
LimitedparallelexecutionIfyouarewonderingifasynccanalsobeusedtolimittheconcurrencyofparalleltasks,theanswerisyes,itcan!Wehaveafewfunctionswecanuseforthat,namely,eachLimit(),mapLimit(),parallelLimit(),queue(),andcargo().
Let'strytoexploitoneofthemtoimplementversion4ofthewebspiderapplication,theoneexecutingthedownloadofthelinksinparallelwithlimitedconcurrency.Fortunately,asynchasasync.queue(),whichworksinasimilarwayastheTaskQueueclasswecreatedearlierinthechapter.Theasync.queue()functioncreatesanewqueue,whichusesaworker()functiontoexecuteasetoftaskswithaspecifiedconcurrencylimit:
varq=async.queue(worker,concurrency);
Theworker()functionreceives,asinput,thetasktorunandacallbackfunctiontoinvoke,whenthetaskcompletes:
functionworker(task,callback)
Weshouldnoticethattaskinthiscasecanbeanything,notjustafunction.Infact,it'stheresponsibilityoftheworkertohandleataskinthemostappropriateway.Newtaskscanbeaddedtothequeuebyusingq.push(task,callback).Thecallbackassociatedtoataskhastobeinvokedbytheworkerafterthetaskhasbeenprocessed.
Now,let'smodifyourcodeagaintoimplementaparallelgloballylimitedexecutionflow,usingasync.queue().Firstofall,weneedtocreateanewqueue:
vardownloadQueue=async.queue(function(taskData,callback){
spider(taskData.link,taskData.nesting-1,callback);
},2);
Thecodeisreallystraightforward.Wearejustcreatinganewqueuewithaconcurrencylimitof2,havingaworkerthatsimplyinvokesourspider()functionwiththedataassociatedwithatask.Next,weimplementthespiderLinks()function:
functionspiderLinks(currentUrl,body,nesting,callback){
if(nesting===0){
returnprocess.nextTick(callback);
}
varlinks=utilities.getPageLinks(currentUrl,body);
if(links.length===0){
returnprocess.nextTick(callback);
}
varcompleted=0,errored=false;
links.forEach(function(link){
vartaskData={link:link,nesting:nesting};
downloadQueue.push(taskData,function(err){
if(err){
errored=true;
returncallback(err);
}
if(++completed===links.length&&!errored){
callback();
}
});
});
}
Theprecedingcodeshouldlookveryfamiliar,asit'salmostthesameastheoneweusedtoimplementthesameflowusingtheTaskQueueobject.Also,inthiscase,theimportantparttoanalyzeiswherewepushanewtaskintothequeue.Atthatpoint,weensurethatwepassacallbackthatenablesustocheckifallthedownloadtasksforthecurrentpagearecompleted,andeventuallyinvokethefinalcallback.
Thankstoasync.queue(),wecouldeasilyreplicatethefunctionalityofourTaskQueueobject,againdemonstratingthatwithasync,wecanreallyavoidwritingasynchronouscontrolflowpatternsfromscratch,reducingoureffortsandsavingpreciouslinesofcode.
PromisesWealreadymentionedatthebeginningofthechapterthatCPSisnottheonlywaytowriteasynchronouscode.Infact,theJavaScriptecosystemprovidesalternativestothetraditionalcallbackpattern.Oneoftheseinparticularisreceivingalotofmomentum,especiallynowthatitisgoingtobepartoftheECMAScript6specification(alsoknownasES6orHarmony),theupcomingversionoftheJavaScriptlanguage.Wearetalking,ofcourse,aboutpromises,andinparticularaboutthoseimplementationsthatfollowthePromises/A+specification(https://promisesaplus.com).
NoteThereareotherpromisesimplementationsthatarenotcomplianttothePromises/A+specification,andamongthose,themostpopularistheoneprovidedbyJQuery.Mostofthetopicsdiscussedinthissectiondonotapplytothosenoncompliantimplementations.
Whatisapromise?Inverysimpleterms,promisesareanabstractionthatallowanasynchronousfunctiontoreturnanobjectcalledapromise,whichrepresentstheeventualresultoftheoperation.Inthepromisesjargon,wesaythatapromiseispendingwhentheasynchronousoperationisnotyetcomplete,it'sfulfilledwhentheoperationsuccessfullycompletes,andrejectedwhentheoperationterminateswithanerror.Onceapromiseiseitherfulfilledorrejected,it'sconsideredsettled.
Toreceivethefulfillmentvalueortheerror(reason)associatedwiththerejection,wecanusethethen()methodofthepromise.Thefollowingisitssignature:
promise.then([onFulfilled],[onRejected])
WhereonFulfilled()isafunctionthatwilleventuallyreceivethefulfillmentvalueofthepromise,andonRejected()isanotherfunctionthatwillreceivethereasonoftherejection(ifany).Bothfunctionsareoptional.
TohaveanideaofhowPromisescantransformourcode,let'sconsiderthefollowing:
asyncOperation(arg,function(err,result){
if(err){
//handleerror
}
//dostuffwithresult
});
PromisesallowtotransformthistypicalCPScodeintoabetterstructuredandmoreelegantcode,suchasthefollowing:
asyncOperation(arg)
.then(function(result){
//dostuffwithresult
},function(err){
//handleerror
});
Onecrucialpropertyofthethen()methodisthatitsynchronouslyreturnsanotherpromise.IfanyoftheonFulfilled()oronRejected()functionsreturnavaluex,thepromisereturnedbythethen()methodwillbeasfollows:
FulfillwithxifxisavalueFulfillwiththefulfillmentvalueofxifxisapromiseorathenableRejectwiththeeventualrejectionreasonofxifxisapromiseorathenable
NoteAthenableisapromise-likeobjectwithathen()method.Thistermisusedtoindicateapromisethatisforeigntotheparticularpromiseimplementationinuse.
Thisfeatureallowsustobuildchainsofpromises,allowingeasyaggregationandarrangementofasynchronousoperationsinseveralconfigurations.Also,ifwedon'tspecifyanonFulfilled()oronRejected()handler,thefulfillmentvalueorrejectionreasonsareautomaticallyforwardedtothenextpromiseinthechain.Thisallowsus,forexample,toautomaticallypropagateerrorsacrossthewholechainuntilcaughtbyanonRejected()handler.Withapromisechain,sequentialexecutionoftaskssuddenlybecomesatrivialoperation:
asyncOperation(arg)
.then(function(result1){
//returnsanotherpromise
returnasyncOperation(arg2);
})
.then(function(result2){
//returnsavalue
return'done';
})
.then(undefined,function(err){
//anyerrorinthechainiscaughthere
});
Thefollowingdiagramprovidesanotherperspectiveonhowapromisechainworks:
AnotherimportantpropertyofpromisesisthattheonFulfilled()andonRejected()functionsareguaranteedtobeinvokedasynchronously,evenifweresolvethepromisesynchronouslywithavalue,aswedidintheprecedingexample,wherewereturnedthestringdoneinthelastthen()functionofthechain.ThisbehaviorshieldsourcodeagainstallthosesituationswherewecouldunintentionallyreleaseZalgo,makingourasynchronouscodemoreconsistentandrobustwithnoeffort.
Nowcomesthebestpart.Ifanexceptionisthrown(usingthethrowstatement)fromtheonFulfilled()oronRejected()handler,thepromisereturnedbythethen()methodwillautomaticallyrejectwiththeexceptionastherejectionreason.ThisisatremendousadvantageoverCPS,asitmeansthatwithpromises,exceptionswillpropagateautomaticallyacrossthechain,andthatthethrowstatementisnotanenemyanymore.
NoteForadetaileddescriptionofthePromises/A+specification,youcanrefertotheofficialwebsitehttp://promises-aplus.github.io/promises-spec/.
Promises/A+implementationsInNode.js,andingeneralinJavaScript,thereareseverallibrariesimplementingthePromises/A+specifications.Thefollowingarethemostpopular:
Bluebird(https://npmjs.org/package/bluebird)
Q(https://npmjs.org/package/q)RSVP(https://npmjs.org/package/rsvp)Vow(https://npmjs.org/package/vow)When.js(https://npmjs.org/package/when)ES6Promises
WhatreallydifferentiatesthemistheadditionalsetoffeaturestheyprovideontopofthePromises/A+standard.Thestandard,infact,definesonlythebehaviorofthethen()methodandthepromiseresolutionprocedure,butitdoesnotspecifyotherfunctionalities,forexample,howapromiseiscreatedfromacallback-basedasynchronousfunction.
Inourexamples,wewilltrytousethesetofAPIimplementedbytheES6promises,astheywillbenativelyavailableinJavaScriptwithoutthesupportofanyexternallibrary.Luckily,theprecedinglistoflibrariesaregraduallyadaptingtosupporttheES6API,sousinganyoneofthemshouldnotforceusintoanystrongimplementationlock-inasfarasweuseonlythefeaturesetoftheES6standard.
NotePleasebearinmindthattheECMAScript6specificationisstilladraftatthetimeofwriting.Sotheremightbesomedifferencesfromwhatwillbethefinalstandard.Also,considerthatatthetimeofwriting,theversionofV8usedbyNode.jsstilldoesnotsupportpromisesnatively.So,forourexamples,wearegoingtouseoneoftheprecedinglistedimplementations,namely,Bluebird.Ofcourse,wewilluseonlythepartofitsAPIthatiscompatiblewithES6promises.
Forreference,hereisthelistoftheAPIscurrentlyprovidedbytheES6promises:
Constructor(newPromise(function(resolve,reject){})):Thiscreatesanewpromisethatfulfillsorrejectsbasedonthebehaviorofthefunctionpassedasanargument.Theargumentsoftheconstructorareexplainedasfollows:
resolve(obj):Thiswillresolvethepromisewithafulfillmentvalue,whichwillbeobjifobjisavalue.Itwillbethefulfillmentvalueofobjifobjisapromiseorathenable.reject(err):Thisrejectsthepromisewiththereasonerr.ItisaconventiontohaveerrtobeaninstanceofError.
StaticmethodsofthePromiseobject:
Promise.resolve(obj):Thiscreatesanewpromisefromathenableora
value.Promise.reject(err):Thiscreatesapromisethatrejectswitherrasthereason.Promise.all(array):Thiscreatesapromisethatfulfillswithanarrayoffulfillmentvalueswheneveryiteminthearrayfulfills,andrejectswiththefirstrejectionreasonifanyitemrejects.Eachiteminthearraycanbeapromise,agenericthenable,oravalue.
MethodsofaPromiseinstance:
promise.then(onFulfilled,onRejected):Thisistheessentialmethodofapromise.ItsbehavioriscompatiblewiththePromises/A+standardwedescribedbefore.promise.catch(onRejected):Thisisjustasyntacticsugarforpromise.then(undefined,onRejected).
NoteItisworthmentioningthatsomepromiseimplementationsofferanothermechanismtocreatenewpromises;thisiscalleddeferreds.Wearenotgoingtodescribeithere,becauseit'snotpartoftheES6standard,butifyouwanttoknowmore,youcanreadthedocumentationforQ(https://github.com/kriskowal/q#using-deferreds)orWhen.js(https://github.com/cujojs/when/wiki/Deferred).
PromisifyingaNode.jsstylefunctionInNode.js,andingeneralinJavaScript,thereareonlyafewlibrariessupportingpromisesout-of-the-box.Mostofthetime,infact,wehavetoconvertatypicalcallback-basedfunctionintoonethatreturnsapromise;thisisalsoknownaspromisification.
Fortunately,thecallbackconventionsusedinNode.jsallowustocreateareusablefunctionthatwecanutilizetopromisifyanyNode.jsstyleAPI.WecandothiseasilybyusingtheconstructorofthePromiseobject.Let'sthencreateanewfunctioncalledpromisify()andincludeitintotheutilities.jsmodule(sowecanuseitlaterinourwebspiderapplication):
varPromise=require('bluebird');
module.exports.promisify=function(callbackBasedApi){
returnfunctionpromisified(){
varargs=[].slice.call(arguments);
returnnewPromise(function(resolve,reject){//[1]
args.push(function(err,result){//[2]
if(err){
returnreject(err);//[3]
}
if(arguments.length<=2){//[4]
resolve(result);
}else{
resolve([].slice.call(arguments,1));
}
});
callbackBasedApi.apply(null,args);//[5]
});
}
};
Theprecedingfunctionreturnsanotherfunctioncalledpromisified(),whichrepresentsthepromisifiedversionofthecallbackBasedApigivenintheinput.Thisishowitworks:
1. Thepromisified()functioncreatesanewpromiseusingthePromiseconstructorandimmediatelyreturnsitbacktothecaller.
2. InthefunctionpassedtothePromiseconstructor,wemakesuretopasstocallbackBasedApi,aspecialcallback.Asweknowthatthecallbackalwayscomeslast,wesimplyappendittotheargumentlist(args)providedtothepromisified()function.
3. Inthespecialcallback,ifwereceiveanerror,weimmediatelyrejectthepromise.4. Ifnoerrorisreceived,weresolvethepromisewithavalueoranarrayofvalues,
dependingonhowmanyresultsarepassedtothecallback.5. Atlast,wesimplyinvokethecallbackBasedApiwiththelistofargumentswe
havebuilt.
NoteMostofthepromiseimplementationsalreadyprovide,out-of-the-box,somesortofhelpertoconvertaNode.jsstyleAPItoonereturningapromise.Forexample,QhasQ.denodeify()andQ.nbind(),BluebirdhasPromise.promisify(),andWhen.jshasnode.lift().
SequentialexecutionAfteralittlebitofnecessarytheory,wearenowreadytoconvertourwebspiderapplicationtousepromises.Let'sstartdirectlyfromversion2,theonedownloading
insequencethelinksofawebpage.
Inthespider.jsmodule,theveryfirststeprequiredistoloadourpromisesimplementation(wewilluseitlater)andpromisifythecallback-basedfunctionsthatweplantouse:
varPromise=require('bluebird');
varutilities=require('./utilities');
varrequest=utilities.promisify(require('request'));
varmkdirp=utilities.promisify(require('mkdirp'));
varfs=require('fs');
varreadFile=utilities.promisify(fs.readFile);
varwriteFile=utilities.promisify(fs.writeFile);
Now,wecanstartconvertingthedownload()function:
functiondownload(url,filename){
console.log('Downloading'+url);
varbody;
returnrequest(url)
.then(function(results){
body=results[1];
returnmkdirp(path.dirname(filename));
})
.then(function(){
returnwriteFile(filename,body);
})
.then(function(){
console.log('Downloadedandsaved:'+url);
returnbody;
});
}
Wecanseestraightawayhowelegantsomesequentialcodeimplementedwithpromisesis;wesimplyhaveanintuitivechainofthen()functions.Thefinalreturnvalueofthedownload()functionisthepromisereturnedbythelastthen()invocationinthechain.Thismakessurethatthecallerreceivesapromisethatfulfillswithbodyonlyafteralltheoperations(request,mkdirp,writeFile)havecompleted.
Next,it'stheturnofthespider()function:
functionspider(url,nesting){
varfilename=utilities.urlToFilename(url);
returnreadFile(filename,'utf8')
.then(
function(body){
returnspiderLinks(url,body,nesting);
},
function(err){
if(err.code!=='ENOENT'){
throwerr;
}
returndownload(url,filename)
.then(function(body){
returnspiderLinks(url,body,nesting);
});
}
);
}
TheimportantthingtonoticehereisthatwealsoregisteredanonRejected()functionforthepromisereturnedbyreadFile(),tohandlethecasewhenapagewasnotalreadydownloaded(filedoesnotexist).Also,it'sinterestingtoseehowwewereabletousethrowtopropagatetheerrorfromwithinthehandler.
Nowthatwehaveconvertedourspider()functionaswell,wecanmodifyitsmaininvocationasfollows:
spider(process.argv[2],1)
.then(function(){
console.log('Downloadcomplete');
})
.catch(function(err){
console.log(err);
});
Notehowweused,forthefirsttime,thesyntacticsugarcatchtohandleanyerrorsituationoriginatedfromthespider()function.Ifwelookagainatallthecodewehavewrittensofarinthissection,wewouldbepleasantlysurprisedbythefactthatwedidn'tincludeanyerrorpropagationlogiclikewewouldbeforcedtodobyusingcallbacks.Thisisclearlyanenormousadvantage,asitgreatlyreducestheboilerplateinourcodeandthechancesofmissinganyasynchronouserror.
Now,theonlymissingbittocompletetheversion2ofourwebspiderapplicationisthespiderLinks()function,whichwearegoingtoseeinamoment.
SequentialiterationThewebspidercodesofarwasmainlyanoverviewofwhatpromisesareandhowtheyareused,demonstratinghowsimpleandelegantitistoimplementasequentialexecutionflowusingpromises.However,thecodeweconsideredsofarinvolvesonlytheexecutionofaknownsetofasynchronousoperations.So,themissingpiecethatwillcompleteourexplorationofsequentialexecutionflowsistoseehowwecanimplementaniterationusingpromises.Again,thespiderLinks()functionofweb
spiderversion2isaperfectexampletoshowthat.
Let'saddthemissingpiecetothecodewewrotesofar:
functionspiderLinks(currentUrl,body,nesting){
varpromise=Promise.resolve();//[1]
if(nesting===0){
returnpromise;
}
varlinks=utilities.getPageLinks(currentUrl,body);
links.forEach(function(link){//[2]
promise=promise.then(function(){
returnspider(link,nesting-1);
});
});
returnpromise;
}
Toiterateasynchronouslyoverallthelinksofawebpage,wehadtodynamicallybuildachainofpromises:
1. First,wedefinedan"empty"promise,resolvingtoundefined.Thispromiseisjustusedasastartingpointtobuildourchain.
2. Then,inaloop,weupdatethepromisevariablewithanewpromiseobtainedbyinvokingthen()onthepreviouspromiseinthechain.Thisisactuallyourasynchronousiterationpatternusingpromises.
Thisway,attheendoftheloop,thepromisevariablewillcontainthepromiseofthelastthen()invocationintheloop,soitwillresolveonlywhenallthepromisesinthechainhavebeenresolved.
Withthis,wecompletelyconvertedourwebspiderversion2tousepromises.Weshouldnowbeabletotryitoutagain.
Sequentialiteration–thepatternToconcludethissectiononsequentialexecution,let'sextractthepatterntoiterateoverasetofpromisesinsequence:
vartasks=[...]
varpromise=Promise.resolve();
tasks.forEach(function(task){
promise=promise.then(function(){
returntask();
});
});
promise.then(function(){
//Alltaskscompleted
});
AnalternativetousingtheforEach()loopistousereduce(),allowinganevenmorecompactcode:
vartasks=[...]
varpromise=tasks.reduce(function(prev,task){
returnprev.then(function(){
returntask();
});
},Promise.resolve());
promise.then(function(){
//Alltaskscompleted
});
Asalways,withsimpleadaptationsofthispattern,wecouldcollectallthetasks'resultsinanarray;wecouldimplementamappingalgorithm,orbuildafilter,andsoon.
NotePattern(sequentialiterationwithpromises):dynamicallybuildsachainofpromisesusingaloop.
ParallelexecutionAnotherexecutionflowthatbecomestrivialwithpromisesistheparallelexecutionflow.Infact,allthatweneedtodoisusethebuilt-inPromise.all()helperthatcreatesanotherpromise,whichfulfillsonlywhenallthepromisesreceivedinaninputarefulfilled.That'sessentiallyaparallelexecutionbecausenoorderbetweenthevariouspromises'resolutionsisenforced.
Todemonstratethis,let'sconsiderversion3ofourwebspiderapplication,theonedownloadingallthelinksofapageinparallel.Let'supdatethespiderLinks()functionagaintoimplementaparallelflow,usingpromises:
functionspiderLinks(currentUrl,body,nesting){
if(nesting===0){
returnPromise.resolve();
}
varlinks=utilities.getPageLinks(currentUrl,body);
varpromises=links.map(function(link){
returnspider(link,nesting-1);
});
returnPromise.all(promises);
}
Trivially,thepatternconsistsinstartingthespider()tasksallatonceintotheelements.map()loop,whichalsocollectsalltheirpromises.Thistime,intheloop,wearenotwaitingforthepreviousdownloadtocompletebeforestartinganewone,allthedownloadtasksarestartedintheloopatonce,oneaftertheother.Afterwards,weleveragedthePromise.all()method,whichreturnsanewpromisethatwillbefulfilledwhenallthepromisesinthearrayarefulfilled.Inotherwords,itfulfillswhenallthedownloadtaskshavecompleted;exactlywhatwewanted.
LimitedparallelexecutionUnfortunately,theES6PromiseAPIdoesnotprovideawaytoimplementalimitedparallelcontrolflownatively,butwecanalwaysrelyonwhatwelearnedaboutlimitingtheconcurrencywithplainJavaScript.Infact,thepatternweimplementedinsidetheTaskQueueclasscanbeeasilyadaptedtosupporttasksthatreturnapromise.Thiscanbedonetriviallybymodifyingthenext()method:
TaskQueue.prototype.next=function(){
varself=this;
while(self.running<self.concurrency&&self.queue.length)
{
vartask=self.queue.shift();
task().then(function(){
self.running--;
self.next();
});
self.running++;
}
}
Sonow,insteadofhandlingthetaskwithacallback,wesimplyinvokethen()onthepromiseitreturns.TherestofthecodeispracticallyidenticaltotheoldversionofTaskQueue.
Now,wecangobacktothespider.jsmodule,modifyingittosupportournewversionoftheTaskQueueclass.First,wemakesuretodefineanewinstanceofTaskQueue:
varTaskQueue=require('./taskQueue');
vardownloadQueue=newTaskQueue(2);
Then,it'stheturnofthespiderLinks()functionagain.Thechangehereisalsoprettystraightforward:
functionspiderLinks(currentUrl,body,nesting){
if(nesting===0){
returnPromise.resolve();
}
varlinks=utilities.getPageLinks(currentUrl,body);
//weneedthefollowingbecausethePromisewecreatenext
//willneversettleiftherearenotaskstoprocess
if(links.length===0){
returnPromise.resolve();
}
returnnewPromise(function(resolve,reject){//[1]
varcompleted=0;
links.forEach(function(link){
vartask=function(){//[2]
returnspider(link,nesting-1)
.then(function(){
if(++completed===links.length){
resolve();
}
})
.catch(reject);
};
downloadQueue.pushTask(task);
});
});
}
Thereareacoupleofthingsintheprecedingcodethatmeritourattention:
1. First,weneededtoreturnanewpromisecreatedusingthePromiseconstructor.Aswewillsee,thisenablesustoresolvethepromisemanually,whenallofthetasksinthequeuearecompleted.
2. Second,weshouldlookathowwedefinedthetask.WhatwedidisattachanonFulfilled()callbacktothepromisereturnedbyspider(),sowecouldcountthenumberofdownloadtaskscompleted.Whentheamountofcompleteddownloadsmatchesthenumberoflinksinthecurrentpage,weknowthatwearedoneprocessing,sowecaninvoketheresolve()functionoftheouterpromise.
Note
ThePromises/A+specificationstatesthattheonFulfilled()andonRejected()callbacksofthethen()methodhavetobeinvokedonlyonceandexclusively(onlyoneortheotherisinvoked).Acompliantpromisesimplementationmakessurethatevenifwecallresolveorrejectmultipletimes,thepromiseiseitherfulfilledorrejectedonlyonce.
Version4ofthewebspiderapplicationusingpromisesshouldnowbereadytobetriedout.Wemightnoticeonceagainhowthedownloadtasksnowruninparallel,withaconcurrencylimitof2.
GeneratorsTheES6specificationintroducesanothermechanismthat,besidesotherthings,canbeusedtosimplifytheasynchronouscontrolflowofourNode.jsapplications.Wearetalkingaboutgenerators,alsoknownassemi-coroutines.Theyareageneralizationofsubroutines,wheretherecanbedifferententrypoints.Inanormalfunction,infact,wecanhaveonlyoneentrypoint,whichcorrespondstotheinvocationofthefunctionitself.Ageneratorissimilartoafunction,butinaddition,itcanbesuspended(usingtheyieldstatement)andthenresumedatalatertime.Generatorsareparticularlyusefulwhenimplementingiterators,andthisshouldringabell,aswehavealreadyseenhowiteratorscanbeusedtoimplementimportantasynchronouscontrolflowpatternssuchassequentialandlimitedparallelexecution.
NoteInNode.js,generatorsareavailablestartingfromVersion0.11,butatthemomentofwriting,thisfeatureisstillnotenabledbydefaultandit'snecessarytoinvokeNode.jswiththe--harmonyor--harmony-generatorsflagstogetgeneratorsworking.Totrytheexamplesinthissection,makesureyouhavetherightversionofNode.jsinstalled(Version0.11.0andlater),byrunningthefollowingcommand:
node--version
ThebasicsBeforeweexploretheuseofgeneratorsforasynchronouscontrolflow,it'simportantwelearnsomebasicconcepts.Let'sstartfromthesyntax;ageneratorfunctioncanbedeclaredbyappendingthe*(asterisk)operatorafterthefunctionkeyword:
function*makeGenerator(){
//body
}
InsidethemakeGenerator()function,wecanpausetheexecutionusingthekeywordyieldandreturntothecallerthevaluepassedtoit:
function*makeGenerator(){
yield'HelloWorld';
console.log('Re-entered');
}
Intheprecedingcode,thegeneratoryieldsastring,HelloWorld,byputtingtheexecutionofthefunctiononpause.Whenthegeneratorisresumed,theexecutionwillstartfromconsole.log('Re-entered').
ThemakeGenerator()functionisessentiallyafactorythat,wheninvoked,returnsanewgeneratorobject:
vargen=makeGenerator();
Themostimportantmethodofthegeneratorobjectisnext(),whichisusedtostart/resumetheexecutionofthegeneratorandreturnsanobjectinthefollowingform:
{
value:<yieldedvalue>
done:<trueiftheexecutionreachedtheend>
}
Thisobjectcontainsthevalueyieldedbythegenerator(value)andaflagtoindicateifthegeneratorhascompleteditsexecution(done).
AsimpleexampleTodemonstrategenerators,let'screateanewmodule.WecancallitfruitGenerator.jsandincludethefollowingcode:
function*fruitGenerator(){
yield'apple';
yield'orange';
return'watermelon';
}
varnewFruitGenerator=fruitGenerator();
console.log(newFruitGenerator.next());//[1]
console.log(newFruitGenerator.next());//[2]
console.log(newFruitGenerator.next());//[3]
Wecanrunthenewmodulewiththefollowingcommand:
node--harmony-generatorsfruitGenerator
Theprecedingcodeshouldprintthefollowingoutput:
{value:'apple',done:false}
{value:'orange',done:false}
{value:'watermelon',done:true}
Thisisashortexplanationofwhathappenedintheprecedingcode:
1. ThefirsttimenewFruitGenerator.next()wasinvoked,thegeneratorstarteditsexecutionuntilitreachedthefirstyieldcommand,whichputthegeneratoronpauseandreturnedthevalueapple,tothecaller.
2. AtthesecondinvocationofnewFruitGenerator.next(),thegeneratorresumed,startingfromthesecondyieldcommand,whichinturnputonpausetheexecutionagain,whilereturningthevalueorangetothecaller.
3. ThelastinvocationofnewFruitGenerator.next()causedtheexecutionofthegeneratortoresumefromitslastinstruction,areturnstatement,whichterminatesthegenerator,returnsthevalue,watermelon,andsetsthedonepropertytotrueintheresultobject.
GeneratorsasiteratorsTobetterunderstandwhygeneratorsaresousefulfortheimplementationofiterators,let'sbuildone.Inanewmodule,whichwewillcalliteratorGenerator.js,let'swritethefollowingcode:
function*iteratorGenerator(arr){
for(vari=0;i<arr.length;i++){
yieldarr[i];
};
}
variterator=iteratorGenerator(['apple','orange',
'watermelon']);
varcurrentItem=iterator.next();
while(!currentItem.done){
console.log(currentItem.value);
currentItem=iterator.next();
}
Wecanexecutethiscodeusingthefollowingcommand:
node--harmony-generatorsiteratorGenerator
Theprecedingsimpleprogramshouldprintthelistoftheitemsinthearrayasfollows:
apple
orange
watermelon
Inthisexample,eachtimewecalliterator.next(),weresumetheforloopofthegenerator,whichrunsanothercyclebyyieldingthenextiteminthearray.Thisdemonstrateshowthestateofthegeneratorismaintainedacrossinvocations.Whenresumed,theloopandallthevariablesareexactlythesameaswhentheexecutionwasputonpause.
PassingvaluesbacktoageneratorToconcludeourexplorationofthebasicfunctionalityofgenerators,wewillnowlearnhowtopassvaluesbacktoagenerator.Thisisactuallyverysimple;whatweneedtodoisjustprovidinganargumenttothenext()method,andthatvaluewillbeprovidedasthereturnvalueoftheyieldstatementinsidethegenerator.
Toshowthis,let'screateanewsimplemodule:
function*twoWayGenerator(){
varwhat=yieldnull;
console.log('Hello'+what);
}
vartwoWay=twoWayGenerator();
twoWay.next();
twoWay.next('world');
Whenexecuted,theprecedingcodewillprintHelloworld.Thismeansthatthefollowinghashappened:
1. Whenthefirstnext()methodisinvoked,thegeneratorreachesthefirstyieldfunctionandisthenputonpause.
2. Whennext('world')isinvoked,thegeneratorresumesfromthepointwhereitwasputonpause,whichisontheyieldinstruction,butthistimewehaveavaluethatispassedbacktothegenerator.Thisvaluewillthenbesetintothewhatvariable.Thegeneratorthenexecutestheconsole.log()instructionandterminates.
Inasimilarway,wecanforceageneratortothrowanexception.Thisismadepossiblebyusingthethrowmethodofthegenerator,asshowninthefollowingexample:
vartwoWay=twoWayGenerator();
twoWay.next();
twoWay.throw(newError());
Usingthislastcodesnippet,thetwoWayGenerator()functionwillthrowanexceptionthemomenttheyieldfunctionreturns.Thisworksexactlyasifanexceptionwas
thrownfrominsidethegenerator,andthismeansthatitcanbecaughtandhandledlikeanyotherexceptionusingatry-catchblock.
AsynchronouscontrolflowwithgeneratorsYoumustbewonderinghowgeneratorscanhelpuswithhandlingasynchronousoperations.Weimmediatelydemonstratethatbycreatingafunctionthatallowsustouseasynchronousfunctionsinsideageneratorandthenresumingtheexecutionofthegeneratorwhentheasynchronousoperationcompletes.WewillcallthisfunctionasyncFlow():
functionasyncFlow(generatorFunction){
functioncallback(err){
if(err){
returngenerator.throw(err);
}
varresults=[].slice.call(arguments,1);
generator.next(results.length>1?results:results[0]);
};
vargenerator=generatorFunction(callback);
generator.next();
}
Theprecedingfunctiontakesageneratorasaninput,instantiatesit,andthenimmediatelystartsitsexecution:
vargenerator=generatorFunction(callback);
generator.next();
ThegeneratorFunction()receivesasinputaspecialcallbackfunctionthatinvokesgenerator.throw()ifanerrorisreceived;otherwise,itresumestheexecutionofthegeneratorbypassingbacktheresultsreceivedinthecallbackfunction:
if(err){
returngenerator.throw(err);
}
varresults=[].slice.call(arguments,1);
generator.next(results.length>1?results:results[0]);
Todemonstratethepowerofthissimplefunction,let'screateanewmodulecalledclone.js,which(stupidly)createsacloneofitself.PastetheasyncFlow()functionwejustcreated,followedbythecoreoftheprogram:
varfs=require('fs');
varpath=require('path');
asyncFlow(function*(callback){
varfileName=path.basename(__filename);
varmyself=yieldfs.readFile(fileName,'utf8',callback);
yieldfs.writeFile('clone_of_'+fileName,myself,
callback);
console.log('Clonecreated');
});
Remarkably,withthehelpoftheasyncFlow()function,wewereabletowriteasynchronouscodeusingalinearapproach,aswewereusingblockingfunctions!Themagicbehindthisresultshouldbeclearbynow.Thecallbackpassedtoeachasynchronousfunctionwillinturnresumethegeneratorassoonastheasynchronousoperationiscomplete.Nothingcomplicated,buttheoutcomeissurelyimpressive.
Therearetwoothervariationsofthistechnique,oneinvolvingtheuseofpromisesandtheotherusingthunks.
NoteAthunkusedingenerator-basedcontrolflowisjustafunctionthatpartiallyappliesalltheargumentsoftheoriginalfunctionexceptitscallback.Thereturnvalueisanotherfunctionthatacceptsonlythecallbackasanargument.Forexample,thethunkifiedversionoffs.readFile()wouldbeasfollows:
functionreadFileThunk(filename,options){
returnfunction(callback){
fs.readFile(filename,options,callback);
}
}
Boththunksandpromisesallowustocreategeneratorsthatdonotneedacallbacktobepassedasanargument;forexample,aversionofasyncFlow()usingthunksmightbethefollowing:
functionasyncFlowWithThunks(generatorFunction){
functioncallback(err){
if(err){
returngenerator.throw(err);
}
varresults=[].slice.call(arguments,1);
varthunk=generator.next(results.length>1?results:
results[0]).value;
thunk&&thunk(callback);
};
vargenerator=generatorFunction();
varthunk=generator.next().value;
thunk&&thunk(callback);
}
Thetrickistoreadthereturnvalueofgenerator.next(),whichcontainsthethunk.Thenextstepistoinvokethethunkitself,byinjectingourspecialcallback.Simple!Thisallowsustowritethefollowingcode:
asyncFlowWithThunks(function*(){
varmyself=yieldreadFileThunk(__filename,'utf8');
yieldwriteFileThunk("cloneofclone.js",myself);
console.log("Clonecreated");
});
Similarly,wecouldimplementaversionofasyncFlow()thatacceptsapromiseasyieldable.WeleavethisasanexerciseasitsimplementationrequiresonlyaminimalchangeintheasyncFlowWithThunks()function.WemayalsoimplementanasyncFlow()functionthatacceptsbothpromisesandthunksasyieldables,usingthesameprinciples.
Generator-basedcontrolflowusingcoAsyoumayguess,theNode.jsecosystemalreadyprovidessomesolutionstohandleasynchronouscontrolflowsusinggenerators.Forexample,suspend(https://npmjs.org/package/suspend)isoneoftheoldestandsupportspromises,thunks,Node.js-stylecallbacks,aswellasrawcallbacks.Also,mostofthepromiseslibrariesweanalyzedearlierinthechapterprovidehelperstousepromiseswithgenerators.
AllthesesolutionsarebasedonthesameprincipleswedemonstratedwiththeasyncFlow()function;so,wemaywanttoreuseoneoftheseinsteadofwritingoneourselves.
Fortheexamplesinthissection,wechosetouseco(https://npmjs.org/package/co),whichiscurrentlyreceivingalotofmomentum.Aflexiblesolution,cosupportsseveraltypesofyieldables,someofwhichare:
ThunksPromisesArrays(parallelexecution)Objects(parallelexecution)Generators(delegation)Generatorfunctions(delegation)
coalsohasitsownecosystemofpackagesincludingthefollowing:
Webframeworks,themostpopularbeingkoa(https://npmjs.org/package/koa)LibrariesimplementingspecificcontrolflowpatternsLibrarieswrappingpopularAPIstosupportco
Wewillusecotoreimplementourwebspiderapplicationusinggenerators.
While,toconvertNode.jsstylefunctionstothunks,wearegoingtousealittlelibrarycalledthunkify(https://npmjs.org/package/thunkify).
SequentialexecutionLet'sstartourpracticalexplorationofgeneratorsandcobymodifyingversion2ofthewebspiderapplication.Theveryfirstthingwewanttodoistoloadourdependenciesandgenerateathunkifiedversionofthefunctionswearegoingtouse.Thesewillgoatthetopofthespider.jsmodule:
varthunkify=require('thunkify');
varco=require('co');
varrequest=thunkify(require('request'));
varfs=require('fs');
varmkdirp=thunkify(require('mkdirp'));
varreadFile=thunkify(fs.readFile);
varwriteFile=thunkify(fs.writeFile);
varnextTick=thunkify(process.nextTick);
Lookingattheprecedingcode,wecansurelynoticesomesimilaritieswiththecodeweusedearlierinthechaptertopromisifysomeAPIs.Inthisregard,itisinterestingtopointoutthatifwedecidedtousethepromisifiedversionofourfunctionsinsteadoftheirthunkifiedalternative,thecodethatwillnowfollowwouldremainexactlythesame,thankstothefactthatcosupportsboththunksandpromisesasyieldableobjects.Infact,ifwewant,wecouldevenuseboththunksandpromisesinthesameapplication,eveninthesamegenerator.Thisisatremendousadvantageintermsofflexibility,asitallowsustousegenerator-basedcontrolflowwithwhateversolutionwealreadyhaveatourdisposal.
Okay,nowlet'sstarttransformingthedownload()functionintoagenerator:
function*download(url,filename){
console.log('Downloading'+url);
varresults=yieldrequest(url);
varbody=results[1];
yieldmkdirp(path.dirname(filename));
yieldwriteFile(filename,body);
console.log('Downloadedandsaved:'+url);
returnbody;
}
Byusinggeneratorsandco,ourdownload()functionsuddenlybecomestrivial.Allwehadtodoisjustconvertitintoageneratorfunctionanduseyieldwhereverwehadanasynchronousfunction(asthunk)toinvoke.
Next,it'stheturnofthespider()function:
function*spider(url,nesting){
varfilename=utilities.urlToFilename(url);
varbody;
try{
body=yieldreadFile(filename,'utf8');
}catch(err){
if(err.code!=='ENOENT'){
throwerr;
}
body=yielddownload(url,filename);
}
yieldspiderLinks(url,body,nesting);
}
Theinterestingdetailtonoticefromthislastfragmentofcodeishowwewereabletouseatry-catchblocktohandleexceptions.Also,wecannowusethrowtopropagateerrors!Anotherremarkablelineiswhereweyieldthedownload()function,whichisnotathunknorapromisifiedfunction,butjustanothergenerator.Thisispossible,thankstoco,whichalsosupportsothergeneratorsasyieldables.
Atlast,wecanalsoconvertspiderLinks(),whereweimplementedaniterationtodownloadthelinksofawebpageinsequence.Withgenerators,thisbecomestrivialaswell:
function*spiderLinks(currentUrl,body,nesting){
if(nesting===0){
returnyieldnextTick();
}
varlinks=utilities.getPageLinks(currentUrl,body);
for(vari=0;i<links.length;i++){
yieldspider(links[i],nesting-1);
};
}
Thereisreallylittletoexplainfromthepreviouscode,thereisnopatterntoshowforthesequentialiteration;generatorsandcoaredoingallthedirtyworkforus,sowewereabletowritetheasynchronousiterationasifwewereusingblocking,directstyleAPIs.
Nowcomesthemostimportantpart,theentrypointofourprogram:
co(function*(){
try{
yieldspider(process.argv[2],1);
console.log('Downloadcomplete');
}catch(err){
console.log(err);
};
})();
Thisistheonlyplacewherewehavetoinvokeco(...)towrapagenerator.Infact,oncewedothat,cowillautomaticallywrapanygeneratorwepasstoayieldstatement,andthiswillhappenrecursively,sotherestoftheprogramistotallyagnostictothefactweareusingco,eventhoughit'sunderthehood.
NoteItisimportanttonoticethattheco()functionreturnsathunk,sowehavetoinvokeittostartthespidertask.
Nowitshouldbepossibletorunourgenerator-basedwebspiderapplication.Justremembertousethe--harmonyor--harmony-generatorsflaginthecommandline:
node--harmony-generatorsspider<URL>
ParallelexecutionThebadnewsaboutgeneratorsisthattheyaregreatforwritingsequentialalgorithms,buttheycan'tbeusedtoparallelizetheexecutionofasetoftasks,atleastnotusingjustyieldandgenerators.Infact,thepatterntouseforthesecircumstancesistosimplyrelyonacallback-basedorpromise-basedfunction,whichinturncaneasilybeyieldedandusedwithgenerators.
Fortunately,forthespecificcaseoftheunlimitedparallelexecution,coalreadyallowsustoobtainitnativelybysimplyyieldinganarrayofpromises,thunks,generators,orgeneratorfunctions.
Withthisinmind,version3ofourwebspiderapplicationcanbeimplementedsimplybyrewritingthespiderLinks()functionasfollows:
function*spiderLinks(currentUrl,body,nesting){
if(nesting===0){
returnnextTick();
}
varlinks=utilities.getPageLinks(currentUrl,body);
vartasks=links.map(function(link){
returnspider(link,nesting-1);
});
yieldtasks;
}
Whatwedidwasjustcollectallthedownloadtasks,whichareessentiallygenerators,andthenyieldontheresultingarray.Allthesetaskswillbeexecutedbycoinparallelandthentheexecutionofourgenerator(spiderLinks)willberesumedwhenallthetasksfinishrunning.
Ifyouthinkwecheatedbyexploitingthefeatureofcothatallowsustoyieldonanarray,wecandemonstratehowthesameparallelflowcanbeachievedusingacallback-basedsolutionsimilartowhatwehavealreadyusedearlierinthechapter.Let'susethistechniquetorewritethespiderLinks()onceagain:
functionspiderLinks(currentUrl,body,nesting){
if(nesting===0){
returnnextTick();
}
//returnsathunk
returnfunction(callback){
varcompleted=0,errored=false;
varlinks=utilities.getPageLinks(currentUrl,body);
if(links.length===0){
returnprocess.nextTick(callback);
}
functiondone(err,result){
if(err&&!errored){
errored=true;
callback(err);
}
if(++completed===links.length&&!errored){
callback();
}
}
for(vari=0;i<links.length;i++){
co(spider(links[i],nesting-1))(done);
};
}
}
Torunthespider()functioninparallel,whichisagenerator,wehadtoconvertitintoathunkandthenexecuteit.Thiswaspossiblebywrappingitwiththeco(...)
function,whichessentiallycreatesathunkoutofagenerator.Thisway,wewereabletoinvokeitinparallelandsetthedone()functionascallback.Usually,allthelibrariesforgenerator-basedcontrolflowhaveasimilarfeature,soyoucanalwaystransformageneratorintoacallback-basedfunctionifneeded.
Tostartmultipledownloadtasksinparallel,wejustreusedthecallback-basedpatternforparallelexecution,whichwedefinedearlierinthechapter.WeshouldalsonoticethatwetransformedthespiderLinks()functiontoathunk(it'snotevenageneratoranymore.)Thisenabledustohaveacallbackfunctiontoinvokewhenalltheparalleltasksarecompleted.
NotePattern(generator-to-thunk):convertsageneratortoathunkinordertobeabletorunitinparallelorutilizeitfortakingadvantageofothercallback-orpromises-basedcontrolflowalgorithms.
LimitedparallelexecutionNowthatweknowhowtomoveincaseofnonsequentialexecutionflows,itshouldbeeasytoplantheimplementationofversion4ofourwebspiderapplication,theoneimposingalimitonthenumberofconcurrentdownloadtasks.Wehaveseveraloptionswecanusetodothat,someofthemareasfollows:
Usethecallback-basedversionoftheTaskQueueclassweimplementedpreviouslyinthechapter.Wewouldneedtojustthunkifyitsfunctionsandanygeneratorwewanttouseasatask.Usethepromises-basedversionoftheTaskQueueclass,andjustmakesurethateachgeneratorwewanttouseasataskisconvertedintoafunctionreturningapromise.Useasync,andthunkifyanyhelperweplantouse,inadditiontoconvertinganygeneratortoacallback-basedfunctionthatcanbeusedbythelibrary.Usealibraryfromthecoecosystem,specificallydesignedforthistypeofflow,suchas,co-limiter(https://npmjs.org/package/co-limiter).Implementacustomalgorithmbasedontheproducer-consumerpattern,thesamethatco-limiterusesinternally.
Foreducationalpurposes,wearegoingtochoosethelastoption,sowecandiveintoapatternthatisoftenassociatedwithcoroutines(butalsothreadsandprocesses).
Producer-consumerpattern
Thegoalistoleverageaqueuetofeedafixednumberofworkers,asmanyastheconcurrencylevelwewanttoset.Toimplementthisalgorithm,wearegoingtotakeasstartingpointtheTaskQueueclasswedefinedearlierinthechapter.Let'sstartgradually;thefirstthingwewanttodoisdefinetheconstructor:
functionTaskQueue(concurrency){
this.concurrency=concurrency;
this.running=0;
this.taskQueue=[];
this.consumerQueue=[];
this.spawnWorkers(concurrency);
}
Noticetheinvocationofthis.spawnWorkers()asthisisthemethodinchargeofstartingtheworkers.Thenextstepis,ofcourse,todefineourworkers;let'sseehowtheylook:
TaskQueue.prototype.spawnWorkers=function(concurrency){
varself=this;
for(vari=0;i<concurrency;i++){
co(function*(){
while(true){
vartask=yieldself.nextTask();
yieldtask;
}
})();
}
}
Ourworkersareverysimple;theyarejustgeneratorswrappedaroundco()andexecutedimmediately,sothateachonecanruninparallel.Internally,eachworkerisrunninganinfiniteloopthatblocks(yield)waitingforanewtasktobeavailableinthequeue(yieldself.nextTask()),andwhenthishappens,ityieldsthetask(whichisanyvalidyieldable)waitingforitscompletion.Youmaybewondering,howcanweactuallywaitforthenexttasktobequeued?TheanswerisinthenextTask()methodthatwearenowgoingtodefine:
TaskQueue.prototype.nextTask=function(){
varself=this;
returnfunction(callback){//[1]
if(self.taskQueue.length!==0){
callback(null,self.taskQueue.shift());//[2]
}else{
self.consumerQueue.push(callback);//[3]
}
}
}
Let'sseewhathappensinthismethod,whichisthecoreofthepattern:
1. Themethodreturnsathunk,whichisavalidyieldableforco.2. Thecallbackofthereturnedthunkisinvokedbyprovidingthenexttaskinthe
taskQueue(ifthereisanyavailable).Thiswillimmediatelyunblockaworker,providingthenexttasktoyieldon.
3. Iftherearenotasksinthequeue,thecallbackitselfispushedintotheconsumerQueue.Bydoingthis,wearepracticallyputtingaworkerinidlemode.ThecallbacksintheconsumerQueuefunctionwillbeinvokedassoonaswehaveanewtasktoprocess,whichwillresumethecorrespondingworker.
Now,toknowhowtheidleworkersintheconsumerQueuefunctionareresumed,weneedtodefinethepushTask()method:
TaskQueue.prototype.pushTask=function(task){
if(this.consumerQueue.length!==0){
this.consumerQueue.shift()(null,task);
}else{
this.taskQueue.push(task);
}
}
Trivially,themethodinvokesthefirstcallbackintheconsumerQueuefunctionifavailable,whichinturnwillunblockaworker.Ifnocallbackisavailable,itmeansthatalltheworkersarebusy,sowesimplyaddanewiteminthetaskQueuefunction.
IntheTaskQueueclasswejustdefined,theworkershavetheroleofconsumers,whilewhoeverusespushTask()canbeconsideredaproducer.Thispatternshowsushowageneratorcanlookverysimilartoathread(oraprocess).Infact,theproducer-consumerinteractionisprobablythemostcommonproblempresentedwhenstudyinginter-processcommunicationtechniques,butaswealreadymentioned,itisalsoacommonusecaseforcoroutines.
LimitingthedownloadtasksconcurrencyNowthatwehaveimplementedourlimitedparallelalgorithmusinggeneratorsandtheproducer-consumerpattern,wecanapplyittolimittheconcurrencyofthedownloadtasksofourwebspiderapplication(version4).First,let'sloadandinitializeaTaskQueueobject:
varTaskQueue=require('./taskQueue')
vardownloadQueue=newTaskQueue(2);
Next,wemodifythespiderLinks()function.Itsbodyisalmostidenticaltotheone
wejustusedtoimplementtheunlimitedparallelexecutionflow,sowewillonlyshowthechangedpartshere:
functionspiderLinks(currentUrl,body,nesting){
[...]
returnfunction(callback){
[...]
functiondone(err,result){
[...]
}
links.forEach(function(link){
downloadQueue.pushTask(function*(){
yieldspider(link,nesting-1);
done();
});
});
}
}
Ineachofthetasks,weinvokethedone()functionjustafteradownloadcompletes,sowecancounthowmanylinksweredownloadedandthennotifythecallbackofthethunkwhenallarecomplete.
Asanexercise,youcantrytoimplementversion4ofthewebspiderapplication,usingtheotherfourmethodswepresentedatthebeginningofthissection.
ComparisonAtthispointofthechapter,weshouldhaveabetterunderstandingofwhatoptionswehavetotametheasynchronousnatureofNode.js.Eachoneofthesolutionspresentedhasitsownprosandcons.Let'ssummarizetheminthefollowingtable:
Solutions Pros Cons
PlainJavaScript
DoesnotrequireanyadditionallibraryortechnologyOffersthebestperformancesProvidesthebestlevelofcompatibilitywiththird-partylibrariesAllowsthecreationofadhocandmoreadvancedalgorithms
Mightrequireextracodeandrelativelycomplexalgorithms
Async
SimplifiesthemostcommoncontrolflowpatternsIsstillacallback-basedsolutionGoodperformances
IntroducesanexternaldependencyMightstillnotbeenoughforadvancedflows
Promises
GreatlysimplifythemostcommoncontrolflowpatternsRobusterrorhandlingPartoftheES6specificationGuaranteedeferredinvocationofonFulfilledandonRejected
MightrequireanexternaldependencyRequiretopromisifycallback-basedAPIsOnlyafewthird-partylibrariessupportpromisesnativelyIntroduceasmallperformancehit
Requireacomplementarycontrolflowlibrary
Generators
MakenonblockingAPIlooklikeblockingSimplifyerrorhandlingPartofES6specification
StillrequirecallbacksorpromisestoimplementnonsequentialflowsNotyetavailablebydefaultonNode.jsRequiretothunkifyorpromisifynongenerator-basedAPIs
NoteItisworthmentioningthatwechosetopresentinthischapteronlythemostpopularsolutionstohandleasynchronouscontrolflow,ortheonesreceivingalotofmomentum,butit'sgoodtoknowthatthereareafewmoreoptionsyoumightwanttolookat,forexample,Fibers(https://npmjs.org/package/fibers)andStreamline(https://npmjs.org/package/streamline).
SummaryAtthebeginningofthischapter,wesaidthatNode.jsprogrammingcanbetoughbecauseofitsasynchronousnature,especiallyforpeopleusedtodevelopingonotherplatforms.However,throughoutthischapterweshowedhowasynchronousAPIscanbebenttoourwill,startingwithplainJavaScript,whichprovidedusthefoundationfortheanalysisofmoresophisticatedtechniques.Wethensawthatthetoolsatourdisposalareindeedvariegatedandprovidegoodsolutionstomostofourproblems,inadditiontoofferingaprogrammingstyleforeverytaste;forexample,wemaychooseasynctosimplifythemostcommonflows,ortotallychangeparadigmbyusingpromiseswiththeirfluentchainingandrobusterrormanagement,orifwewanttogetfancy,wecanalwaysleveragegeneratorsandfeellikeweareprogrammingwithblockingAPIs.
Thischaptershouldnotonlyhavetaughtyouhowtochoosebetweenoneortheothersolutionsbutalsohowtousethemtogether,eveninthesameproject.
Thischapter,atlast,shouldhavegivenusthatconfidenceneededforefficientlyreadingandwritingasynchronouscodeandshouldhavebroughtusastepclosertomasteringNode.jsprogramming.
Inthenextchapter,wewillintroducestreams,theswissarmyknifeofNode.js.WewillseehowtheycanbeusednotonlytoefficientlyhandleI/O,butalsoasatoolfortransformingdataandobjects.
Chapter3.CodingwithStreamsStreamsareoneofthemostimportantcomponentsandpatternsofNode.js.ThereisamottointhecommunitythatsaysStreamallthethings!andthisaloneshouldbeenoughtodescribetheroleofstreamsinNode.js.DominicTarr,atopcontributortotheNode.jscommunity,definesstreamsasnode'sbestandmostmisunderstoodidea.TherearedifferentreasonsthatmakeNode.jsstreamssoattractive;again,it'snotjustrelatedtotechnicalproperties,suchasperformanceorefficiency,butit'ssomethingmoreabouttheireleganceandthewaytheyfitperfectlyintotheNode.jsphilosophy.
Inthischapter,youwilllearnaboutthefollowingtopics:
WhystreamsaresoimportantinNode.jsUsingandcreatingstreamsStreamsasaprogrammingparadigm:leveragingtheirpowerinmanydifferentcontextsandnotjustforI/OPipingpatternsandconnectingstreamstogetherindifferentconfigurations
DiscoveringtheimportanceofstreamsInanevent-basedplatformsuchasNode.js,themostefficientwaytohandleI/Oisinrealtime,consumingtheinputassoonasitisavailableandsendingtheoutputassoonasitisproducedbytheapplication.
Inthissection,wearegoingtogiveaninitialintroductiontoNode.jsstreamsandtheirstrengths.Pleasebearinmindthatthisisonlyanoverview,asamoredetailedanalysisonhowtouseandcomposestreamswillfollowlaterinthechapter.
BufferingvsStreamingAlmostalltheasynchronousAPIsthatwe'veseensofarinthebookworkusingthebuffermode.Foraninputoperation,thebuffermodecausesallthedatacomingfromaresourcetobecollectedintoabuffer;itisthenpassedtoacallbackassoonastheentireresourceisread.Thefollowingdiagramshowsavisualexampleofthisparadigm:
Intheprecedingfigure,wecanseethat,atthetimet1,somedatawasreceivedfromtheresourceandsavedintothebuffer.Atthetimet2,anotherdatachunkisreceived—thefinalone—thatcompletesthereadoperationandcausestheentirebuffertobesenttotheconsumer.
Ontheotherside,streamsallowyoutoprocessthedataassoonasitarrivesfromtheresource.Thisisshowninthefollowingdiagram:
Thistime,thediagramshowsyouhoweachnewchunkofdataisreceivedfromtheresourceandisimmediatelyprovidedtotheconsumer,whonowhasthechancetoprocessitstraightawaywithoutwaitingforallthedatatobecollectedinthebuffer.
Butwhatarethedifferencesbetweenthetwoapproaches?Wecansummarizethemintwomajorcategories:
SpatialefficiencyTimeefficiency
However,Node.jsstreamshaveanotherimportantadvantage:composability.Let'snowseewhatimpactthesepropertieshaveinthewaywedesignandwriteourapplications.
SpatialefficiencyFirstofall,streamsallowustodothingsthatwouldnotbepossible,bybufferingdataandprocessingitallatonce.Forexample,considerthecaseinwhichwehavetoreadaverybigfile,let'ssay,intheorderofhundredsofmegabytesorevengigabytes.Clearly,usinganAPIthatreturnsabigbufferwhenthefileiscompletelyread,isnotagoodidea.Imaginereadingafewofthesebigfilesconcurrently;ourapplicationwilleasilyrunoutofmemory.Besidesthat,buffersinV8cannotbebiggerthan0x3FFFFFFFbytes(alittlebitlessthan1GB).So,wemighthitawallwaybeforerunningoutofphysicalmemory.
GzippingusingabufferedAPITomakeaconcreteexample,let'sconsiderasimpleCommand-lineInterface(CLI)applicationthatcompressesafileusingtheGzipformat.UsingabufferedAPI,suchanapplicationwilllooklikethefollowinginNode.js(errorhandlingisomittedforbrevity):
varfs=require('fs');
varzlib=require('zlib');
varfile=process.argv[2];
fs.readFile(file,function(err,buffer){
zlib.gzip(buffer,function(err,buffer){
fs.writeFile(file+'.gz',buffer,function(err){
console.log('Filesuccessfullycompressed');
});
});
});
Now,wecantrytoputtheprecedingcodeinafilenamedgzip.jsandthenrunitwiththefollowingcommand:
nodegzip<pathtofile>
Ifwechooseafilethatisbigenough,let'ssayalittlebitbiggerthan1GB,wewillreceiveaniceerrormessagesayingthatthefilethatwearetryingtoreadisbiggerthanthemaximumallowedbuffersize,suchasthefollowing:
RangeError:FilesizeisgreaterthanpossibleBuffer:
0x3FFFFFFFbytes
That'sexactlywhatweexpected,andit'sasymptomthatweareusingthewrongapproach.
GzippingusingstreamsThesimplestwaywehavetofixourgzipapplicationandmakeitworkwithbigfilesistouseastreamingAPI.Let'sseehowthiscanbeachieved;let'sreplacethecontentsofthemodulewejustcreatedwiththefollowingcode:
varfs=require('fs');
varzlib=require('zlib');
varfile=process.argv[2];
fs.createReadStream(file)
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream(file+'.gz'))
.on('finish',function(){
console.log('Filesuccessfullycompressed');
});
Isthatit?Youmayask.Yes,aswesaid,streamsareamazingalsobecauseoftheirinterfaceandcomposability,thusallowingclean,elegant,andconcisecode.Wewillseethisinawhileinmoredetail,butfornowtheimportantthingtorealizeisthattheprogramwillrunsmoothlyagainstfilesofanysize,ideallywithconstantmemoryutilization.Tryityourself(butconsiderthatcompressingabigfilemighttakeawhile).
TimeefficiencyLet'snowconsiderthecaseofanapplicationthatcompressesafileanduploadsittoaremoteHTTPserver,whichinturndecompressesandsavesitonthefilesystem.IfourclientwasimplementedusingabufferedAPI,theuploadwouldstartonlywhentheentirefilehasbeenreadandcompressed.Ontheotherhand,thedecompressionwillstartontheserveronlywhenallthedatahasbeenreceived.Abettersolutiontoachievethesameresultinvolvestheuseofstreams.Ontheclientmachine,streamsallowsyoutocompressandsendthedatachunksassoonastheyarereadfromthefilesystem,whereas,ontheserver,itallowsyoutodecompresseverychunkassoonasitisreceivedfromtheremotepeer.Let'sdemonstratethisbybuildingtheapplicationthatwementionedearlier,startingfromtheserverside.
Let'screateamodulenamedgzipReceive.jscontainingthefollowingcode:
varhttp=require('http');
varfs=require('fs');
varzlib=require('zlib');
varserver=http.createServer(function(req,res){
varfilename=req.headers.filename;
console.log('Filerequestreceived:'+filename);
req
.pipe(zlib.createGunzip())
.pipe(fs.createWriteStream(filename))
.on('finish',function(){
res.writeHead(201,{'Content-Type':'text/plain'});
res.end('That\'sit\n');
console.log('Filesaved:'+filename);
});
});
server.listen(3000,function(){
console.log('Listening');
});
Theserverreceivesthedatachunksfromthenetwork,decompressesthem,andsavesthemassoonastheyarereceived,thankstoNode.jsstreams.
TheclientsideofourapplicationwillgointoamodulenamedgzipSend.jsanditlookslikethefollowing:
varfs=require('fs');
varzlib=require('zlib');
varhttp=require('http');
varpath=require('path');
varfile=process.argv[2];
varserver=process.argv[3];
varoptions={
hostname:server,
port:3000,
path:'/',
method:'PUT',
headers:{
filename:path.basename(file),
'Content-Type':'application/octet-stream',
'Content-Encoding':'gzip'
}
};
varreq=http.request(options,function(res){
console.log('Serverresponse:'+res.statusCode);
});
fs.createReadStream(file)
.pipe(zlib.createGzip())
.pipe(req)
.on('finish',function(){
console.log('Filesuccessfullysent');
});
Intheprecedingcode,weareagainusingstreamstoreadthedatafromthefile,thencompressandsendeachchunkassoonasitisreadfromthefilesystem.
Now,totryouttheapplication,let'sfirststarttheserverusingthefollowingcommand:
nodegzipReceive
Then,wecanlaunchtheclientbyspecifyingthefiletosendandtheaddressoftheserver(forexamplelocalhost):
nodegzipSend<pathtofile>localhost
Ifwechooseafilebigenough,wewillseemoreeasilyhowthedataflowsfromtheclienttotheserver,butwhyexactlyisthisparadigmwherewehaveflowingdatamoreefficientcomparedtousingabufferedAPI?Thefollowingdiagramshouldgiveusahint:
Whenafileisprocesseditgoesthroughasetofsequentialstages:
1. [Client]Readfromthefilesystem.2. [Client]Compressthedata.3. [Client]Sendittotheserver.4. [Server]Receivefromtheclient.5. [Server]Decompressthedata.6. [Server]Writethedatatodisk.
Tocompletetheprocessing,wehavetogothrougheachstagelikeinanassemblyline,insequence,untiltheend.Intheprecedingfigure,wecanseethat,usingabufferedAPI,theprocessisentirelysequential.Tocompressthedata,wefirsthavetowaitfortheentirefiletoberead,then,tosendthedata,wehavetowaitfortheentirefiletobebothreadandcompressed,andsoon.Wheninsteadweareusingstreams,theassemblylineiskickedoffassoonaswereceivethefirstchunkofdata,withoutwaitingfortheentirefiletoberead.Butmoreamazingly,whenthenextchunkofdataisavailable,thereisnoneedtowaitfortheprevioussetoftaskstobecompleted;instead,anotherassemblylineislaunchedinparallel.Thisworksperfectlybecauseeachtaskthatweexecuteisasynchronous,soitcanbeparallelizedbyNode.js;theonlyconstraintisthattheorderinwhichthechunksarriveineachstagemustbepreserved(andNode.jsstreamstakecareofthisforus).
Aswecanseefromthepreviousfigure,theresultofusingstreamsisthattheentireprocesstakeslesstime,becausewewastenotimeinwaitingforallthedatatobereadandprocessedallatonce.
ComposabilityThecodewehaveseensofarhasalreadygivenusanoverviewofhowstreamscanbecomposed,thankstothepipe()method,whichallowsustoconnectthedifferentprocessingunits,eachbeingresponsibleforonesinglefunctionalityinperfectNode.jsstyle.Thisispossiblebecausestreamshaveauniforminterface,andtheycanunderstandeachotherintermsofAPI.Theonlyprerequisiteisthatthenextstreaminthepipelinehastosupportthedatatypeproducedbythepreviousstream,whichcanbeeitherbinary,text,orevenobjects,aswewillseelaterinthechapter.
Totakealookatanotherdemonstrationofthepowerofthisproperty,wecantrytoaddanencryptionlayertothegzipReceive/gzipSendapplicationthatwebuiltpreviously.
Todothis,weonlyneedtoupdatetheclientbyaddinganotherstreamtothepipeline;tobeprecise,thestreamreturnedbycrypto.createChipher().Theresultingcodeshouldbeasfollows:
varcrypto=require('crypto');
[...]
fs.createReadStream(file)
.pipe(zlib.createGzip())
.pipe(crypto.createCipher('aes192','a_shared_secret'))
.pipe(req)
.on('finish',function(){
console.log('Filesuccesfullysent');
});
Inasimilarway,wecanupdatetheserversothatthedataisdecryptedbeforebeingdecompressed:
varcrypto=require('crypto');
[...]
varserver=http.createServer(function(req,res){
[...]
req
.pipe(crypto.createDecipher('aes192','a_shared_secret'))
.pipe(zlib.createGunzip())
.pipe(fs.createWriteStream(filename))
.on('finish',function(){
[...]
});
});
Withverylittleeffort(justafewlinesofcode),weaddedanencryptionlayertoourapplication;wesimplyhadtoreuseanalreadyavailabletransformstreamby
includingitinthepipelinethatwealreadyhad.Inasimilarway,wecanaddandcombineotherstreams,asifwewereplayingwithLegobricks.
Clearly,themainadvantageofthisapproachisreusability,butaswecanseefromthecodewepresentedsofar,streamsalsoenablecleanerandmoremodularcode.Forthesereasons,streamsareoftenusednotjusttodealwithpureI/O,butalsoasameanstosimplifyandmodularizethecode.
GettingstartedwithstreamsIntheprevioussection,welearnedwhystreamsaresopowerful,butalsothattheyareeverywhereinNode.js,startingfromitscoremodules.Forexample,wehaveseenthatthefsmodulehascreateReadStream()forreadingfromafileandcreateWriteStream()forwritingtoafile,thehttprequestandresponseobjectsareessentiallystreams,andthezlibmoduleallowsustocompressanddecompressdatausingastreaminginterface.
Nowthatweknowwhystreamsaresoimportant,let'stakeastepbackandstarttoexploretheminmoredetail.
AnatomyofstreamsEverystreaminNode.jsisanimplementationofoneofthefourbaseabstractclassesavailableinthestreamcoremodule:
stream.Readable
stream.Writable
stream.Duplex
stream.Transform
EachstreamclassisalsoaninstanceofEventEmitter.Streams,infact,canproduceseveraltypesofevents,suchasend,whenaReadablestreamhasfinishedreading,orerror,whensomethinggoeswrong.
NotePleasenotethat,forbrevity,intheexamplespresentedinthischapter,wewilloftenomitpropererrormanagement.However,inproductionapplicationsitisalwaysadvisedtoregisteranerroreventlistenerforallyourstreams.
Oneofthereasonswhystreamsaresoflexibleisthefactthattheycanhandlenotonlybinarydata,butpractically,almostanyJavaScriptvalue;infacttheycansupporttwooperatingmodes:
Binarymode:Thismodeiswheredataisstreamedintheformofchunks,suchasbuffersorstringsObjectmode:Thismodeiswherethestreamingdataistreatedasasequenceofdiscreetobjects(allowingtousealmostanyJavaScriptvalue)
ThesetwooperatingmodesallowustousestreamsnotonlyforI/O,butalsoasatooltoelegantlycomposeprocessingunitsinafunctionalfashion,aswewillseelaterinthechapter.
NoteInthischapter,wewilldiscussmainlytheNode.jsstreaminterfacealsoknownasVersion2,whichwasintroducedinNode.js0.10.Forfurtherdetailsaboutthedifferenceswiththeoldinterface,pleaserefertotheofficialNode.jsblogathttp://blog.nodejs.org/2012/12/20/streams2/.
ReadablestreamsAreadablestreamrepresentsasourceofdata;inNode.js,it'simplementedusingtheReadableabstractclassthatisavailableinthestreammodule.
ReadingfromastreamTherearetwowaystoreceivethedatafromaReadablestream:non-flowingandflowing.Let'sanalyzethesemodesinmoredetail.
Thenon-flowingmode
ThedefaultpatternforreadingfromaReadablestreamconsistsofattachingalistenerforthereadableeventthatsignalstheavailabilityofnewdatatoread.Then,inaloop,wereadallthedatauntiltheinternalbufferisemptied.Thiscanbedoneusingtheread()method,whichsynchronouslyreadsfromtheinternalbufferandreturnsaBufferorStringobjectrepresentingthechunkofdata.Theread()methodhasthefollowingsignature:
readable.read([size])
Usingthisapproach,thedataisexplicitlypulledfromthestreamondemand.
Toshowhowthisworks,let'screateanewmodulenamedreadStdin.js,whichimplementsasimpleprogramthatreadsfromthestandardinput(aReadablestream)andechoeseverythingbacktothestandardoutput:
process.stdin
.on('readable',function(){
varchunk;
console.log('Newdataavailable');
while((chunk=process.stdin.read())!==null){
console.log(
'Chunkread:('+chunk.length+')"'+
chunk.toString()+'"'
);
}
})
.on('end',function(){
process.stdout.write('Endofstream');
});
Theread()methodisasynchronousoperationthatpullsadatachunkfromtheinternalbuffersoftheReadablestream.Thereturnedchunkis,bydefault,aBufferobjectifthestreamisworkinginbinarymode.
NoteInaReadablestreamworkinginbinarymode,wecanreadstringsinsteadofbuffersbycallingsetEncoding(encoding)onthestream,andprovideavalidencodingformat(forexample,utf8).
Thedataisreadexclusivelyfromwithinthereadablelistener,whichisinvokedassoonasnewdataisavailable.Theread()methodreturnsnullwhenthereisnomoredataavailableintheinternalbuffers;insuchacase,wehavetowaitforanotherreadableeventtobefired-tellingusthatwecanreadagain-orwaitfortheendeventthatsignalstheendofthestream.Whenastreamisworkinginbinarymode,wecanalsospecifythatweareinterestedinreadingaspecificamountofdatabypassingasizevaluetotheread()method.Thisisparticularlyusefulwhenimplementingnetworkprotocolsorwhenparsingspecificdataformats.
Now,wearereadytorunthereadStdinmoduleandexperimentwithit.Let'stypesomecharactersintheconsoleandthenpressEntertoseethedataechoedbackintothestandardoutput.Toterminatethestreamandhencegenerateagracefulendevent,weneedtoinsertanEOF(End-Of-File)character(usingCtrl+ZonWindowsorCtrl+DonLinux).
Wecanalsotrytoconnectourprogramwithotherprocesses;thisispossibleusingthepipeoperator(|),whichredirectsthestandardoutputofaprogramtothestandardinputofanother.Forexample,wecanrunacommandsuchasthefollowing:
cat<pathtoafile>|nodereadStdin
Thisisanamazingdemonstrationofhowthestreamingparadigmisauniversal
interface,whichenablesourprogramstocommunicate,regardlessofthelanguagetheyarewrittenin.
Theflowingmode
Anotherwaytoreadfromastreamisbyattachingalistenertothedataevent;thiswillswitchthestreamintousingtheflowingmodewherethedataisnotpulledusingread(),butinsteadit'spushedtothedatalistenerassoonasitarrives.Forexample,thereadStdinapplicationthatwecreatedearlierwilllooklikethisusingtheflowingmode:
process.stdin
.on('data',function(chunk){
console.log('Newdataavailable');
console.log(
'Chunkread:('+chunk.length+')"'+
chunk.toString()+'"'
);
})
.on('end',function(){
process.stdout.write('Endofstream');
});
Theflowingmodeisaninheritanceoftheoldversionofthestreaminterface(alsoknownasStreams1),andofferslessflexibilitytocontroltheflowofdata.WiththeintroductionoftheStreams2interface,theflowingmodeisnotthedefaultworkingmode;toenableit,it'snecessarytoattachalistenertothedataeventorexplicitlyinvoketheresume()method.Totemporarilystopthestreamfromemittingdataevents,wecantheninvokethepause()method,causinganyincomingdatatobecachedintheinternalbuffer.
NoteCallingpause()doesnotcausethestreamtoswitchbacktothenon-flowingmode.
ImplementingReadablestreamsNowthatweknowhowtoreadfromastream,thenextstepistolearnhowtoimplementanewReadablestream.Todothis,it'snecessarytocreateanewclassbyinheritingtheprototypeofstream.Readable.Theconcretestreammustprovideanimplementationofthe_read()method,whichhasthefollowingsignature:
readable._read(size)
TheinternalsoftheReadableclasswillcallthe_read()method,whichinturnwillstarttofilltheinternalbufferusingpush():
readable.push(chunk)
NotePleasenotethatread()isamethodcalledbythestreamconsumers,while_read()isamethodtobeimplementedbyastreamsubclassandshouldneverbecalleddirectly.Theunderscoreusuallyindicatesthatthemethodisnotpublicandshouldnotbecalleddirectly.
TodemonstratehowtoimplementthenewReadablestreams,wecantrytoimplementastreamthatgeneratesrandomstrings.Let'screateanewmodulecalledrandomStream.jsthatwillcontainthecodeofourstringgenerator.Atthetopofthefile,wewillloadourdependencies:
varstream=require('stream');
varutil=require('util');
varchance=require('chance').Chance();
Nothingspecialhere,exceptthatweareloadinganpmmodulecalledchance(https://npmjs.org/package/chance),whichisalibraryforgeneratingallsortsofrandomvalues,fromnumberstostringstoentiresentences.
ThenextstepistocreateanewclasscalledRandomStreamandthatspecifiesstream.Readableasitsparent:
functionRandomStream(options){
stream.Readable.call(this,options);
}
util.inherits(RandomStream,stream.Readable);
Intheprecedingcode,wecalltheconstructoroftheparentclasstoinitializeitsinternalstate,andforwardtheoptionsargumentreceivedasinput.Thepossibleparameterspassedthroughtheoptionsobjectinclude:
TheencodingargumentthatisusedtoconvertBufferstoStrings(defaultstonull)Aflagtoenabletheobjectmode(objectModedefaultstofalse)
Theupperlimitofthedatastoredintheinternalbufferafterwhichnomorereadingfromthesourceshouldbedone(highWaterMarkdefaultsto16KB)
Okay,nowthatwehaveournewRandomStreamconstructorready,wecanproceedwithimplementingthe_read()method:
RandomStream.prototype._read=function(size){
varchunk=chance.string();//[1]
console.log('Pushingchunkofsize:'+chunk.length);
this.push(chunk,'utf8');//[2]
if(chance.bool({likelihood:5})){//[3]
this.push(null);
}
}
module.exports=RandomStream;
Theprecedingmethodisexplainedasfollows:
1. Themethodgeneratesarandomstringusingchance.2. Itpushesthestringintotheinternalreadingbuffer.Notethat,sinceweare
pushingaString,wealsospecifytheencoding,utf8(thisisnotnecessaryifthechunkissimplyabinaryBuffer).
3. Itterminatesthestreamrandomly,withalikelihoodof5percent,bypushingnullintotheinternalbuffertoindicateanEOFsituationor,inotherwords,theendofthestream.
Wecanalsoseethatthesizeargumentgivenininputtothe_read()functionisignored,asitisanadvisoryparameter.Wecansimplyjustpushalltheavailabledata,butiftherearemultiplepushesinsidethesameinvocation,thenweshouldcheckwhetherpush()returnsfalse,asthiswouldmeanthattheinternalbufferhasreachedthehighWaterMarklimitandweshouldstopaddingmoredatatoit.
That'sitforRandomStream;wearenotreadytouseit.Let'screateanewmodulenamedgenerateRandom.jsinwhichweinstantiateanewRandomStreamobjectandpullsomedatafromit:
varRandomStream=require('./randomStream');
varrandomStream=newRandomStream();
randomStream.on('readable',function(){
varchunk;
while((chunk=randomStream.read())!==null){
console.log("Chunkreceived:"+chunk.toString());
}
});
Now,everythingisreadyforustotryournewcustomstream.Simplyexecutethe
generateRandommoduleasusualandwatcharandomsetofstringsflowingonthescreen.
WritablestreamsAwritablestreamrepresentsadatadestination;inNode.js,it'simplementedusingtheWritableabstractclass,whichisavailableinthestreammodule.
WritingtoastreamPushingsomedatadownawritablestreamisastraightforwardbusiness;allweneedtodoistousethewrite()method,whichhasthefollowingsignature:
writable.write(chunk,[encoding],[callback])
TheencodingargumentisoptionalandcanbespecifiedifchunkisString(defaultstoutf8,ignoredifchunkisBuffer);thecallbackfunctioninsteadiscalledwhenthechunkisflushedintotheunderlyingresourceandisoptionalaswell.
Tosignalthatnomoredatawillbewrittentothestream,wehavetousetheend()method:
writable.end([chunk],[encoding],[callback])
Wecanprovideafinalchunkofdatathroughtheend()method;inthiscasethecallbackfunctionisequivalenttoregisteringalistenertothefinishevent,whichisfiredwhenallthedatawritteninthestreamhasbeenflushedintotheunderlyingresource.
Now,let'sshowhowthisworksbycreatingasmallHTTPserverthatoutputsarandomsequenceofstrings:
varchance=require('chance').Chance();
require('http').createServer(function(req,res){
res.writeHead(200,{'Content-Type':'text/plain'});//[1]
while(chance.bool({likelihood:95})){//[2]
res.write(chance.string()+'\n');//[3]
}
res.end('\nTheend...\n');//[4]
res.on('finish',function(){//[5]
console.log('Alldatawassent');
});
}).listen(8080,function(){
console.log('Listening');
});
TheHTTPserverthatwecreatedwritesintotheresobject,whichisaninstanceofhttp.ServerResponseandalsoaWritablestream.Whathappensisexplainedasfollows:
1. WefirstwritetheheadoftheHTTPresponse.NotethatwriteHead()isnotapartoftheWritableinterface;infact,it'sanauxiliarymethodexposedbythehttp.ServerResponseclass.
2. Westartaloopthatterminateswithalikelihoodoffivepercent(weinstructchance.bool()toreturntruefor95percentofthetime).
3. Insidetheloop,wewritearandomstringintothestream.4. Onceweareoutoftheloop,wecallend()onthestream,indicatingthatno
moredatawillbewritten.Also,weprovideafinalstringtobewrittenintothestreambeforeendingit.
5. Finally,weregisteralistenerforthefinishevent,whichwillbefiredwhenallthedatahasbeenflushedintotheunderlyingsocket.
Wecancallthissmallmodule,entropyServer.js,andthenexecuteit.Totesttheserver,wecanopenabrowserattheaddresshttp://localhost:8080,orusecurlfromtheterminalasfollows:
curllocalhost:8080
Atthispoint,theservershouldstartsendingrandomstringstotheHTTPclientthatyouchose(pleasebearinmindthatsomebrowsersmightbufferthedata,andthestreamingbehaviormightnotbeapparent).
NoteAninterestingcuriosityisthefactthathttp.ServerResponseisactuallyaninstanceoftheoldStreamclass(http://nodejs.org/docs/v0.8.0/api/stream.html).It'simportanttostate,though,thatthisdoesnotaffectourexample,astheinterfaceandbehavioronthewritablesideremainalmostthesameinthenewerstream.Writableclass.
Back-pressureSimilartoaliquidflowinginarealpipingsystem,Node.jsstreamscanalsosufferfrombottlenecks,wheredataiswrittenfasterthanthestreamcanconsumeit.Themechanismtocopewiththisproblemconsistsofbufferingtheincomingdata;however,ifthestreamdoesn'tgiveanyfeedbacktothewriter,wemightincura
situationwheremoreandmoredataisaccumulatedintotheinternalbuffer,leadingtoundesiredlevelsofmemoryusage.
Topreventthisfromhappening,writable.write()willreturnfalsewhentheinternalbufferexceedsthehighWaterMarklimit.TheWritablestreamshaveahighWaterMarkproperty,whichisthelimitoftheinternalbuffersizebeyondwhichthewrite()methodstartsreturningfalse,indicatingthattheapplicationshouldnowstopwriting.Whenthebufferisemptied,thedraineventisemitted,communicatingthatit'ssafetostartwritingagain.Thismechanismiscalledback-pressure.
NoteThemechanismdescribedinthissectionissimilarlyapplicabletoReadablestreams.Infact,back-pressureexistsintheReadablestreamstoo,andit'striggeredwhenthepush()method,whichisinvokedinside_read(),returnsfalse.However,it'saproblemspecifictostreamimplementers,sowewilldealwithitlessfrequently.
Wecanquicklydemonstratehowtotakeintoaccounttheback-pressureofaWritablestream,bymodifyingtheentropyServerthatwecreatedbefore:
varchance=require('chance').Chance();
require('http').createServer(function(req,res){
res.writeHead(200,{'Content-Type':'text/plain'});
functiongenerateMore(){//[1]
while(chance.bool({likelihood:95})){
varshouldContinue=res.write(
chance.string({length:(16*1024)–1})//[2]
);
if(!shouldContinue){//[3]
console.log('Backpressure');
returnres.once('drain',generateMore);
}
}
res.end('\nTheend...\n',function(){
console.log('Alldatawassent');
});
}
generateMore();
}).listen(8080,function(){
console.log('Listening');
});
Themostimportantstepsofthepreviouscodecanbesummarizedasfollows:
1. WewrappedthemainlogicintoafunctioncalledgenerateMore().2. Toincreasethechancesofreceivingsomeback-pressure,weincreasedthesize
ofthedatachunkto16KB-1Byte,whichisveryclosetothedefaulthighWaterMarklimit.
3. Afterwritingachunkofdata,wecheckthereturnvalueofres.write();ifwereceivefalse,itmeansthattheinternalbufferisfullandweshouldstopsendingmoredata.Inthiscase,weexitfromthefunction,andregisteranothercycleofwritesforwhenthedraineventisemitted.
Ifwenowtrytoruntheserveragain,andthengenerateaclientrequestwithcurl,thereisagoodprobabilitythattherewillbesomeback-pressure,astheserverproducesdataataveryhighrate,fasterthantheunderlyingsocketcanhandle.
ImplementingWritablestreamsWecanimplementanewWritablestreambyinheritingtheprototypeofstream.Writableandprovidinganimplementationforthe_write()method.Let'strytodoitimmediatelywhilediscussingthedetailsalongtheway.
Let'sbuildaWritablestreamthatreceivesobjectsinthefollowingformat:
{
path:<pathtoafile>
content:<stringorbuffer>
}
Foreachoneoftheseobjects,ourstreamhastosavethecontentpartintoafilecreatedatthegivenpath.Wecanimmediatelyseethattheinputofourstreamareobjects,andnotstringsorbuffers;thismeansthatourstreamhastoworkinobjectmode.
Let'scallthemoduletoFileStream.jsand,asthefirststep,let'sloadallthedependenciesthatwearegoingtouse:
varstream=require('stream');
varfs=require('fs');
varutil=require('util');
varpath=require('path');
varmkdirp=require('mkdirp');
Next,wehavetocreatetheconstructorofournewstream,whichinheritstheprototypefromstream.Writable:
functionToFileStream(){
stream.Writable.call(this,{objectMode:true});
};
util.inherits(ToFileStream,stream.Writable);
Again,wehadtoinvoketheparentconstructortoinitializeitsinternalstate;wealsoprovideanoptionsobjectthatspecifiesthatthestreamworksinanobjectmode(objectMode:true).Otheroptionsacceptedbystream.Writableareasfollows:
highWaterMark(thedefaultis16KB):Thiscontrolstheback-pressurelimit.decodeStrings(defaultstotrue):Thisenablestheautomaticdecodingofstringsintobinarybuffersbeforepassingthemtothe_write()method.Thisoptionisignoredintheobjectmode.
Finally,weneedtoprovideanimplementationforthe_write()method:
ToFileStream.prototype._write=function(chunk,encoding,
callback){
varself=this;
mkdirp(path.dirname(chunk.path),function(err){
if(err){
returncallback(err);
}
fs.writeFile(chunk.path,chunk.content,callback);
});
}
module.exports=ToFileStream;
Thisisagoodtimetoanalyzethesignatureofthe_write()method.Asyoucansee,themethodacceptsadatachunk,anencoding(whichmakessenseonlyifweareinthebinarymodeandthestreamoptiondecodeStringsissettofalse).Also,themethodacceptsacallbackfunction,whichneedstobeinvokedwhentheoperationcompletes;it'snotnecessarytopasstheresultoftheoperationbut,ifneeded,wecanstillpassanerrorthatwillcausethestreamtoemitanerrorevent.
Now,totrythestreamthatwejustbuilt,wecancreateanewmodule,calledforexample,writeToFile.js,andperformsomewriteoperationsagainstthestream:
varToFileStream=require('./toFileStream');
vartfs=newToFileStream();
tfs.write({path:"file1.txt",content:"Hello"});
tfs.write({path:"file2.txt",content:"Node.js"});
tfs.write({path:"file3.txt",content:"Streams"});
tfs.end(function(){
console.log("Allfilescreated");
});
Withthis,wecreatedandusedourfirstcustomWritablestream.Runthenewmoduleasusualtocheckitsoutput.
DuplexstreamsADuplexstreamisastreamthatisbothReadableandWritable.Itisusefulwhenwewanttodescribeanentitythatisbothadatasourceandadatadestination,asforexample,networksockets.Duplexstreamsinheritthemethodsofbothstream.Readableandstream.Writable,sothisisnothingnewtous.Thismeansthatwecanread()orwrite()data,orlistenforboththereadableanddrainevents.
TocreateacustomDuplexstream,wehavetoprovideanimplementationforboth_read()and_write();theoptionsobjectpassedtotheDuplex()constructorisinternallyforwardedtoboththeReadableandWritableconstructors.Theoptionsarethesameasthosewealreadydiscussedintheprevioussections,withtheadditionofanewonecalledallowHalfOpen(defaultstotrue)thatifsettofalsewillcauseboththeparts(ReadableandWritable)ofthestreamtoendifonlyoneofthemdoes.
NoteTohaveaDuplexstreamworkingintheobjectmodeononesideandbinarymodeontheother,weneedtomanuallysetthefollowingpropertiesfromwithinthestreamconstructor:
this._writableState.objectMode
this._readableState.objectMode
TransformstreamsTheTransformstreamsareaspecialkindofDuplexstreamthatarespecificallydesignedtohandledatatransformations.
InasimpleDuplexstream,thereisnoimmediaterelationshipbetweenthedatareadfromthestreamandthedatawrittenintoit(atleast,thestreamisagnostictosucharelationship).ThinkaboutaTCPsocket,whichjustsendsandreceivesdatatoandfromtheremotepeer;thesocketisnotawareofanyrelationshipbetweentheinputandoutput.ThefollowingdiagramillustratesthedataflowinaDuplexstream:
Ontheotherside,TransformstreamsapplysomekindoftransformationtoeachchunkofdatathattheyreceivefromtheirWritablesideandthenmakethetransformeddataavailableontheirReadableside.ThefollowingdiagramshowshowthedataflowsinaTransformstream:
Fromtheoutside,theinterfaceofaTransformstreamisexactlylikethatofaDuplexstream.However,whenwewanttobuildanewDuplexstreamwehavetoprovidethe_read()and_write()methodswhile,forimplementinganewTransformstream,wehavetofillinanotherpairofmethods:_transform()and_flush().
Let'sshowhowtocreateanewTransformstreamwithanexample.
ImplementingTransformstreamsLet'simplementaTransformstreamthatreplacesalltheoccurrencesofagivenstring.Todothis,wehavetocreateanewmodulenamedreplaceStream.js.Asalways,wewillstartbuildingthemodulefromitsdependencies,creatingtheconstructorandextendingitsprototypewiththeparentstreamclass:
varstream=require('stream');
varutil=require('util');
functionReplaceStream(searchString,replaceString){
stream.Transform.call(this,{decodeStrings:false});
this.searchString=searchString;
this.replaceString=replaceString;
this.tailPiece='';
}
util.inherits(ReplaceStream,stream.Transform);
Weassumethatthestreamwillhandleonlytext,soweinitializetheparentconstructorbysettingthedecodeStringsoptionstofalse;thisallowsustoreceivestringsinsteadofbuffersinsidethe_transform()method.
Now,let'simplementthe_transform()methoditself:
ReplaceStream.prototype._transform=function(chunk,encoding,
callback){
varpieces=(this.tailPiece+chunk)//[1]
.split(this.searchString);
varlastPiece=pieces[pieces.length-1];
vartailPieceLen=this.searchString.length-1;
this.tailPiece=lastPiece.slice(-tailPieceLen);//[2]
pieces[pieces.length-1]=lastPiece.slice(0,-
tailPieceLen);
this.push(pieces.join(this.replaceString));//[3]
callback();
}
The_transform()methodhaspracticallythesamesignatureasthatofthe_write()methodoftheWritablestreambut,insteadofwritingdataintoanunderlyingresource,itpushesitintotheinternalbufferusingthis.push(),exactlyaswewoulddointhe_read()methodofaReadablestream.ThisconfirmshowthetwosidesofaTransformstreamareactuallyconnected.
The_trasform()methodofReplaceStreamimplementsthecoreofouralgorithm.Tosearchandreplaceastringinabufferisaneasytask;however,it'satotallydifferentstorywhenthedataisstreamingandpossiblematchesmightbedistributedacrossmultiplechunks.Theprocedurefollowedbythecodecanbeexplainedasfollows:
1. OuralgorithmsplitsthechunkusingthesearchStringfunctionasaseparator.2. Then,ittakesthelastitemofthearraygeneratedbytheoperationandextracts
thelastsearchString.length-1characters.TheresultissavedintothetailPiecevariableanditwillbeprependedtothenextchunkofdata.
3. Finally,allthepiecesresultingfromsplit()arejoinedtogetherusingreplaceStringasaseparatorandpushedintotheinternalbuffer.
Whenthestreamends,wemightstillhavealasttailPiecevariablenotpushedintotheinternalbuffer.That'sexactlywhatthe_flush()methodisfor;itisinvokedjustbeforethestreamisendedandthisiswherewehaveonefinalchancetofinalizethestreamorpushanyremainingdatabeforecompletelyendingthestream.Let'simplementittocompleteourReplaceStreamclass:
ReplaceStream.prototype._flush=function(callback){
this.push(this.tailPiece);
callback();
}
module.exports=ReplaceStream;
The_flush()methodtakesinonlyacallbackthatwehavetomakesuretoinvokewhenalltheoperationsarecomplete,causingthestreamtobeterminated.Withthis,wecompletedourReplaceStreamclass.
Now,it'stimetotrythenewstream.WecancreateanothermodulecalledreplaceStreamTest.jsthatwritessomedataandthenreadsthetransformedresult:
varReplaceStream=require('./replaceStream');
varrs=newReplaceStream('World','Node.js');
rs.on('data',function(chunk){
console.log(chunk);
});
rs.write('HelloW');
rs.write('orld!');
rs.end();
Tomakelifealittlebitharderforourstream,wespreadthesearchterm(whichisWorld)acrosstwodifferentchunks;thenusingtheflowingmodewereadfromthesamestream,loggingeachtransformedchunk.Runningtheprecedingprogramshouldproducethefollowingoutput:
Hel
loNode.js
!
NoteThereisafifthtypeofstreamthatisworthmentioning:stream.PassThrough.Unliketheotherstreamclassesthatwepresented,PassThroughisnotabstractandcanbeinstantiatedstraightawaywithouttheneedtoimplementanymethod.Itis,infact,aTransformstreamthatoutputseverydatachunkwithoutapplyinganytransformation.
ConnectingstreamsusingpipesTheconceptofUnixpipeswasinventedbyDouglasMcllroy;thisenabledtheoutputofaprogramtobeconnectedtotheinputofthenext.Takealookatthefollowingcommand:
echoHelloWorld!|seds/World/Node.js/g
Intheprecedingcommand,echowillwriteHelloWorld!toitsstandardoutput,whichisthenredirectedtothestandardinputofthesedcommand(thankstothepipe|operator);thensedreplacesanyoccurrenceofWorldwithNode.jsandprintstheresulttoitsstandardoutput(which,thistime,istheconsole).
Inasimilarway,Node.jsstreamscanbeconnectedtogetherusingthepipe()methodoftheReadablestream,whichhasthefollowinginterface:
readable.pipe(writable,[options])
Veryintuitively,thepipe()methodtakesthedatathatisemittedfromthereadablestreamandpumpsitintotheprovidedwritablestream.Also,thewritablestreamisendedautomaticallywhenthereadablestreamemitsanendevent(unless,wespecify{end:false}asoptions).Thepipe()methodreturnsthewritablestreampassedasanargumentallowingustocreatechainedinvocationsifsuchastreamisalsoReadable(asforexampleaDuplexorTransformstream).
Pipingtwostreamstogetherwillcreateasuctionwhichallowsthedatatoflowautomaticallytothewritablestream,sothereisnoneedtocallread()orwrite();butmostimportantlythereisnoneedtocontroltheback-pressureanymore,becauseit'sautomaticallytakencareof.
Tomakeaquickexample(therewillbetonsofthemcoming),wecancreateanewmodulecalledreplace.jswhichtakesatextstreamfromthestandardinput,appliesthereplacetransformation,andthenpushesthedatabacktothestandardoutput:
varReplaceStream=require('./replaceStream');
process.stdin
.pipe(newReplaceStream(process.argv[2],process.argv[3]))
.pipe(process.stdout);
TheprecedingprogrampipesthedatathatcomesfromthestandardinputintoaReplaceStreamandthenbacktothestandardoutput.Now,totrythissmallapplication,wecanleverageaUnixpipetoredirectsomedataintoitsstandardinput,asshowninthefollowingexample:
echoHelloWorld!|nodereplaceWorldNode.js
Thisshouldproducethefollowingoutput:
HelloNode.js
Thissimpleexampledemonstratesthatstreams(andinparticulartextstreams)isan
universalinterface,andpipesarethewaytocomposeandinterconnectalmostmagicallyalltheseinterfaces.
NoteTheerroreventsarenotpropagatedautomaticallythroughthepipeline.Takeforexamplethiscodefragment:
stream1
.pipe(stream2)
.on('error',function(){});
Intheprecedingpipeline,wewillcatchonlytheerrorscomingfromstream2,whichisthestreamthatweattachedthelistenerto.Thismeansthat,ifwewanttocatchanyerrorgeneratedfromstream1,wehavetoattachanothererrorlistenerdirectlytoit.Wewilllaterseeapatternthatmitigatesthisinconvenience(combiningstreams).Also,weshouldnoticethatifthedestinationstreamemitsanerroritgetsautomaticallyunpipedfromthesourcestream,causingthepipelinetobreak.
UsefulpackagesforworkingwithstreamsWenowpresentsomenpmpackagesthatmightbeveryusefulwhenworkingwithstreams.
readable-streamWealreadymentionedhowthestreamsinterfacechangedconsiderablybetweenthe0.8and0.10branchesofNode.js.Traditionally,theinterfacesupporteduntilNode.js0.8iscalledStreams1,whilethenewerinterfacesupportedbyNode.js0.10iscalledStreams2.Thecoreteamdidagreatjobinmaintainingbackward-compatibility,sothatapplicationsimplementedusingtheStreams1interfacewillcontinuetoworkwiththe0.10branch;however,theviceversaisnottrue,sousingStreams2againstNode.js0.8willnotwork.Also,theupcoming0.12releasewillprobablybeshippedwithanewversionofthestreaminterface,Streams3,andsoonuntiltheinterfacestabilizes.
NoteThestreamsinterface,asofversion0.10,isstillmarkedasunstableonthe
officialdocumentation(http://nodejs.org/docs/v0.10.0/api/stream.html).
Thankfully,wehaveanoptiontoshieldourcodefromthesechanges;it'scalledreadable-stream(https://npmjs.org/package/readable-stream),anpmpackagethatmirrorstheStreams2andStreams3implementationsoftheNode.jscore.Inparticular,usingthe1.0branchofreadable-streamwecanhavetheStreams2interfaceavailableevenifwerunourcodeagainstNode.js0.8.Ifinsteadwechoosethe1.1branch(probably1.2whenNode.js0.12willbereleased)wegettheStreams3interfaceregardlessoftheversionoftheNode.jsplatformused.
Thereadable-streampackageisadrop-inreplacementforthecorestreammodule(dependingontheversion),sousingitisassimpleasrequiringreadable-streaminsteadofstream:
varstream=require('readable-stream');
varReadable=stream.Readable;
varWritable=stream.Writable;
varDuplex=stream.Duplex;
varTransform=stream.Transform;
Protectingourlibrariesandapplicationsfromthechangesofthestillunstablestreamsinterfacecangreatlyreducethedefectsthatoriginatefromplatformincompatibilities.
NoteForadetailedrationaleontheuseofreadable-streamyoucanrefertothisexcellentarticlewrittenbyRodVagg:http://www.nearform.com/nodecrunch/dont-use-nodes-core-stream-module/
throughandfromThewaywecreatedcustomstreamssofardoesnotexactlyfollowtheNodeway;infact,inheritingfromabasestreamclassviolatesthesmallsurfaceareaprincipleandrequiressomeboilerplatecode.Thisdoesnotmeanthatstreamswerebadlydesigned;infact,weshouldnotforgetthatsincetheyareapartoftheNode.jscoretheymustbeasflexibleaspossibleinordertoenableuserlandmodulestoextendthemforabroadrangeofpurposes.
However,mostofthetimewedon'tneedallthepowerandextensibilitythatprototypalinheritancecangive,butusuallywhatwewantisjustaquickandanexpressivewaytodefinenewstreams.TheNode.jscommunity,ofcourse,createda
solutionalsoforthis.Aperfectexampleisthrough2(https://npmjs.org/package/through2),asmalllibrarywhichsimplifiesthecreationofTransformstreams.Withthrough2,wecancreateanewTransformstreambyinvokingasimplefunction:
vartransform=through2([options],[_transform],[_flush])
Inasimilarway,from2(https://npmjs.org/package/from2)allowsustoeasilyandsuccinctlycreateReadablestreamswithcodesuchasthefollowing:
varreadable=from2([options],_read)
Theadvantagesofusingtheselittlelibrarieswillbeimmediatelyclearassoonaswestartshowingtheirusageintherestofthechapter.
NoteThepackagesthrough(https://npmjs.org/package/through)andfrom(https://npmjs.org/package/from)aretheoriginallibrariesbuiltontopofStreams1.
AsynchronouscontrolflowwithstreamsGoingthroughtheexamplesthatwepresentedsofar,itshouldbeclearthatstreamscanbeusefulnotonlytohandleI/O,butalsoasanelegantprogrammingpatternthatcanbeusedtoprocessanykindofdata.Buttheadvantagesdonotendatthesimpleappearance;streamscanalsobeleveragedtoturnasynchronouscontrolflowintoflowcontrol,aswewillseeinthissection.
SequentialexecutionBydefault,streamswillhandledatainasequence,forexample,a_transform()functionofaTransformstreamwillneverbeinvokedagainwiththenextchunkofdata,untilthepreviousinvocationcompletesbyexecutingcallback().Thisisanimportantpropertyofstreams,crucialforprocessingeachchunkintherightorder,butitcanalsobeexploitedtoturnstreamsintoanelegantalternativetothetraditionalcontrolflowpatterns.
Somecodeisalwaysbetterthantoomuchexplanation,solet'sworkonanexampletodemonstratehowwecanusestreamstoexecuteasynchronoustasksinasequence.Let'screateafunctionthatconcatenatesasetoffilesreceivedasinput,makingsuretohonortheorderinwhichtheyareprovided.Let'screateanewmodulecalledconcatFiles.jsandlet'sdefineitscontentsstartingfromitsdependencies:
varfromArray=require('from2-array');
varthrough=require('through2');
varfs=require('fs');
Wewillbeusingthrough2tosimplifythecreationofTransformstreamsandfrom2-arrayinordertocreateareadablestreamfromanarrayofobjects.
Next,wecandefinetheconcatFiles()function:
functionconcatFiles(destination,files,callback){
vardestStream=fs.createWriteStream(destination);
fromArray.obj(files)//[1]
.pipe(through.obj(function(file,enc,done){//[2]
varsrc=fs.createReadStream(file);
src.pipe(destStream,{end:false});
src.on('end',function(){//[3]
done();
});
}))
.on('finish',function(){//[4]
destStream.end();
callback();
});
}
module.exports=concatFiles;
Theprecedingfunctionimplementsasequentialiterationoverthefilesarraybytransformingitintoastream.Theprocedurefollowedbythefunctionisexplainedasfollows:
1. First,weusefrom2-arraytocreateaReadablestreamfromthefilesarray.2. Next,wecreateathrough(Transform)streamtohandleeachfileinthe
sequence.Foreachfile,wecreateaReadablestreamandwepipeitintodestStream,whichrepresentstheoutputfile.WemakesurenottoclosedestStreamafterthesourcefilefinishesreading,byspecifying{end:false}intothepipe()options.
3. WhenallthecontentsofthesourcefilehavebeenpipedintodestStream,weinvokedone(),whichtriggerstheprocessingofthenextfile.
4. Whenallthefileshavebeenprocessed,thefinisheventisfired;wecanfinallyenddestStreamandinvokethecallback()functionofconcatFiles(),whichsignalsthecompletionofthewholeoperation.
Wecannowtrytousethelittlemodulewejustcreated.Let'sdothatinanewfile,calledconcat.js:
varconcatFiles=require('./concatFiles');
concatFiles(process.argv[2],process.argv.slice(3),function()
{
console.log('Filesconcatenatedsuccesfully');
});
Wecannowruntheprecedingprogrambypassingthedestinationfileasthefirstcommandlineargumentfollowedbyalistoffilestoconcatenate,forexample:
nodeconcatallTogether.txtfile1.txtfile2.txt
ThisshouldcreateanewfilecalledallTogether.txtcontaining,inorder,thecontentsoffile1.txtandfile2.txt.
WiththeconcatFiles()function,wewereabletoobtainanasynchronoussequentialiterationusingonlystreams.AswesawinChapter2,AsynchronousControlFlowPatterns,thiswouldhaverequiredtheuseofaniterator,ifimplementedwithpureJavaScript,oranexternallibrarysuchasasync.Wehavenowprovidedanother
optionforachievingthesameresult,whichasweseeisalsoverycompactandelegant.
NotePattern:Useastream,orcombinationofstreams,toeasilyiterateoverasetofasynchronoustasksinsequence.
UnorderedparallelexecutionWejustsawthatstreamsprocesseachdatachunkinasequence,butsometimesthiscanbeabottleneckaswewouldnotmakethemostoftheNode.jsconcurrency.Ifwehavetoexecuteaslowasynchronousoperationforeverydatachunk,itcanbeadvantageoustoparallelizetheexecutionandspeeduptheoverallprocess.Ofcourse,thispatterncanonlybeappliedifthereisnorelationshipbetweeneachchunkofdata,whichmighthappenfrequentlyforobjectstreams,butveryrarelyforbinarystreams.
NoteCaution:parallelstreamscannotbeusedwhentheorderinwhichthedataisprocessedisimportant.
ToparallelizetheexecutionofaTransformstream,wecanapplythesamepatternsthatwelearnedinChapter2,AsynchronousControlFlowPatterns,butwithsomeadaptationstogetthemworkingwithstreams.Let'sseehowthisworks.
ImplementinganunorderedparallelstreamLet'sdemonstratethisimmediatelywithanexample;let'screateamodulecalledparallelStream.jsanddefineagenericTransformstreamthatexecutesagiventransformfunctioninparallel.Let'sstarttodefineitsconstructor:
varstream=require('stream');
varutil=require('util');
functionParallelStream(userTransform){
stream.Transform.call(this,{objectMode:true});
this.userTransform=userTransform;
this.running=0;
this.terminateCallback=null;
}
util.inherits(ParallelStream,stream.Transform);
TheconstructoracceptsauserTransform()function,whichisthensavedasaninstancevariable;wealsoinvoketheparentconstructorandforconvenienceweenabletheobjectmodebydefault.
Next,itistheturnofthe_transform()method:
ParallelStream.prototype._transform=
function(chunk,enc,done){
this.running++;
this.userTransform(chunk,enc,
this._onComplete.bind(this));
done();
}
Inthe_transform()method,weexecutetheuserTransform()function,thenweincrementthecountofrunningtasks;finally,wenotifythatthecurrenttransformationstepiscompletebyinvokingdone().Thetrickfortriggeringtheprocessingofanotheriteminparallelisexactlythis;wearenotwaitingfortheuserTransform()functiontocompletebeforeinvokingdone(),insteadwedoitimmediately.Ontheotherhand,weprovideaspecialcallbacktouserTransform(),whichisthethis._onComplete()method(wearegoingtodefineitinamoment);thisallowsustogetnotifiedwhentheuserTransform()completes.
Next,itistheturnofthe_flush()method:
ParallelStream.prototype._flush=function(done){
if(this.running>0){
this.terminateCallback=done;
}else{
done();
}
}
The_flush()methodisinvokedjustbeforethestreamterminates,soiftherearestilltasksrunningwecanputonholdthereleaseofthefinisheventbynotinvokingthedone()callbackimmediately;instead,weassignittothethis.terminateCallbackvariable.Tounderstandhowthestreamisthenproperlyterminated,wehavetolookintothe_onComplete()method:
ParallelStream.prototype._onComplete=function(err){
this.running--;
if(err){
returnthis.emit('error',err);
}
if(this.running===0){
this.terminateCallback&&this.terminateCallback();
}
}
module.exports=ParallelStream;
The_onComplete()methodisinvokedeverytimeanasynchronoustaskcompletes.Itcheckswhetherthereareanymoretasksrunningand,iftherearenone,itinvokesthethis.terminateCallback()function,whichwillcausethestreamtoend,releasingthefinisheventwhichwasputonholdinthe_flush()method.
TheParallelStreamclass,wejustbuiltallowsustoeasilycreateaTransformstreamwhichexecutesitstasksinparallel,butthereisacaveat:itdoesnotpreservetheorderoftheitemsastheyarereceived.Infact,asynchronousoperationscancompleteandpushdataatanytime,regardlessofwhentheyarestarted.Weimmediatelyunderstandthatthispropertydoesnotplaywellwithbinarystreamswheretheorderofdatausuallymatters,butitcansurelybeusefulwithsometypesofobjectstreams.
ImplementingaURLstatusmonitoringapplicationNow,let'sapplyourParallelStreamtoaconcreteexample.Let'simaginethatwewantedtobuildasimpleservicetomonitorthestatusofabiglistofURLs.Let'simaginealltheseURLsarecontainedinasinglefileandarenewlineseparated.
Streamscanofferaveryefficientandelegantsolutiontothisproblem,especiallyifweuseourParallelStreamtoparallelizethecheckingoftheURLs.
Let'sbuildthissimpleapplicationimmediatelyinanewmodulecalledcheckUrls.js:
varfs=require('fs');
varsplit=require('split');
varrequest=require('request');
varParallelStream=require('./parallelStream');
fs.createReadStream(process.argv[2])//[1]
.pipe(split())//[2]
.pipe(newParallelStream(function(url,enc,done){
//[3]
if(!url)returndone();
varself=this;
request.head(url,function(err,response){
self.push(url+'is'+(err?'down':'up')+'\n');
done();
});
}))
.pipe(fs.createWriteStream('results.txt'))//[4]
.on('finish',function(){
console.log('Allurlswerechecked');
});
Aswecansee,withstreamsourcodelooksveryelegantandstraightforward;let'sseehowitworks:
1. First,wecreateaReadablestreamfromthefilegivenasinput.2. Wepipethecontentsoftheinputfilethroughsplit
(https://npmjs.org/package/split),aTransformstreamthatensuresoutputtingeachlineonadifferentchunk.
3. Then,it'sthetimetouseourParallelStreamtochecktheURL.WedothisbysendingaHEADrequestandwaitingforaresponse.Whenthecallbackisinvoked,wepushtheresultoftheoperationdownthestream.
4. Finally,alltheresultsarepipedintoafile,results.txt.
Now,wecanrunthecheckUrlsmodulewithacommandsuchasthis:
nodecheckUrlsurlList.txt
WherethefileurlList.txtcontainsalistofURLs,forexample:
http://www.example.com
http://www.example.com/link1
http://thiswillbedownforsure.com
Whenthecommandfinishesrunning,wewillseethatafileresults.txtwascreated.Thiscontainstheresultsoftheoperation,forexample:
http://thiswillbedownforsure.comisdown
http://www.example.com/link1isup
http://www.example.comisup
ThereisagoodprobabilitythattheorderinwhichtheresultsarewrittenisdifferentfromtheorderinwhichtheURLswerespecifiedintheinputfile.Thisisclearevidencethatourstreamexecutesitstasksinparallel,anditdoesnotenforceanyorderbetweenthevariousdatachunksinthestream.
NoteForthesakeofcuriosity,wemightwanttotryreplacingParallelStreamwithanormalthrough2stream,andcomparethebehaviorandperformancesofthetwo(youmightwanttodothisasanexercise).Wewillseethatusingthrough2isway
moreslower,becauseeachURLwouldbecheckedinasequence,butalsothattheorderoftheresultsinthefileresults.txtwouldbepreserved.
UnorderedlimitedparallelexecutionIfwetrytorunthecheckUrlsapplicationagainstafilethatcontainsthousandsormillionsofURLs,wewillsurelyrunintotrouble.Ourapplicationwillcreateanuncontrollednumberofconnectionsallatonce,sendingaconsiderableamountofdatainparallelandpotentiallyunderminingthestabilityoftheapplicationandtheavailabilityoftheentiresystem.Aswealreadyknow,thesolutiontokeeptheloadandresourceusageundercontrolistolimittheconcurrencyoftheparalleltasks.
Let'sseehowthisworkswithstreamsbycreatingalimitedParallelStream.jsmodule,whichisanadaptationofparallelStream.jsthatwecreatedintheprevioussection.
Let'sseehowitlookslike,startingfromitsconstructor(wewillhighlightthechangedparts):
functionLimitedParallelStream(concurrency,userTransform){
stream.Transform.call(this,{objectMode:true});
this.userTransform=userTransform;
this.running=0;
this.terminateCallback=null;
this.continueCallback=null;
this.concurrency=concurrency;
}
Weneedaconcurrencylimittobetakenastheinputandthistimewearegoingtosavetwocallbacks,oneforanypending_transformmethod(continueCallback)andanotheroneforthecallbackofthe_flushmethod(terminateCallback).
Nextisthe_transform()method:
LimitedParallelStream.prototype._transform=
function(chunk,enc,done){
this.running++;
this.userTransform(chunk,enc,
this._onComplete.bind(this));
if(this.running<this.concurrency){
done();
}else{
this.continueCallback=done;
}
}
Thistimeinthe_transform()method,wehavetocheckwhetherwehaveanyfreeexecutionslotsbeforeweinvokedone()andtriggertheprocessingofthenextitem.Ifwehavealreadyreachedthemaximumnumberofconcurrentrunningstreams,wecansimplysavethedone()callbackintothecontinueCallbackvariable,sothatitcanbeinvokedassoonasataskfinishes.
The_flush()methodremainsexactlythesameasintheParallelStreamclass,solet'smovedirectlytoimplementingthe_onComplete()method:
LimitedParallelStream.prototype._onComplete=
function(err,chunk){
this.running--;
if(err){
returnthis.emit('error',err);
}
vartmpCallback=this.continueCallback;
this.continueCallback=null;
tmpCallback&&tmpCallback();
if(this.running===0){
this.terminateCallback&&this.terminateCallback();
}
}
EverytimeataskcompletesweinvokeanysavedcontinueCallback()thatwillcausethestreamtounblock,triggeringtheprocessingofthenextitem.
That'sitforthelimitedParallelStreammodule;wecannowuseitinthecheckUrlsmoduleinplaceofparallelStreamandhavetheconcurrencyofourtaskslimitedtothevaluethatweset.
OrderedparallelexecutionTheparallelstreamsthatwecreatedpreviouslymightshuffletheorderoftheemitteddata,buttherearesituationswherethisisnotacceptable;sometimes,infact,itisnecessarytohaveeachchunkemittedinthesameorderinwhichitwasreceived.Butnotallthehopesarelost,wecanstillrunthetransformfunctioninparallel;allwehavetodoistosortthedataemittedbyeachtasksothatitfollowsthesameorderinwhichthedatawasreceived.
Thistechniqueinvolvestheuseofabuffertoreorderthechunkswhiletheyareemittedbyeachrunningtask.Forbrevity,wearenotgoingtoprovideanimplementationofsuchastream,asit'squiteverboseforthescopeofthisbook;whatwearegoingtodoinsteadisreuseoneoftheavailablepackagesonnpmbuiltforthisspecificpurpose,forexample,through2-parallel(https://npmjs.org/package/through2-parallel).
WecanquicklycheckthebehaviorofanorderedparallelexecutionbymodifyingourexistingcheckUrlsmodule.Let'ssaythatwewantourresultstobewritteninthesameorderastheURLsintheinputfile,whileexecutingourchecksinparallel.Wecandothisusingthrough2-parallel:
[...]
varthroughParallel=require('through2-parallel');
fs.createReadStream(process.argv[2])
.pipe(split())
.pipe(throughParallel.obj({concurrency:2},
function(url,enc,done){
[...]
})
)
.pipe(fs.createWriteStream('results.txt'))
.on('finish',function(){
console.log('Allurlswerechecked');
});
Aswesee,theinterfaceofthrough2-parallelisverysimilartothatofthrough2;theonlydifferenceisthatwecanalsospecifyaconcurrencylimitforthetransformfunctionthatweprovide.IfwetrytorunthisnewversionofcheckUrls,wewillnowseethattheresults.txtfileliststheresultsinthesameorderastheURLsappearintheinputfile.
NoteItisimportanttoseethat,eventhoughtheorderoftheoutputisthesameastheinput,theasynchronoustasksstillruninparallelandcanpossiblycompleteinanyorder.
Withthis,weconcludeouranalysisoftheasynchronouscontrolflowwithstreams;nextwearegoingtofocusonsomepipingpatterns.
PipingpatternsAsinreal-lifeplumbing,Node.jsstreamsalsocanbepipedtogetherfollowingdifferentpatterns;wecan,infact,mergetheflowoftwodifferentstreamsintoone,splittheflowofonestreamintotwoormorepipes,orredirecttheflowbasedonacondition.Inthissection,wearegoingtoexplorethemostimportantplumbingtechniquesthatcanbeappliedtoNode.jsstreams.
CombiningstreamsInthischapter,westressedalotonthefactthatstreamsprovideasimpleinfrastructuretomodularizeandreuseourcode,butthereisonelastpiecemissinginthispuzzle:whatifwewanttomodularizeandreuseanentirepipeline?Whatifwewanttocombinemultiplestreamssothattheylooklikeonefromtheoutside?Thefollowingfigureshowswhatthismeans:
Fromtheprecedingdiagram,weshouldalreadygetahintofhowthisworks:
Whenwewriteintothecombinedstream,weareactuallywritingintothefirststreamofthepipelineWhenwereadfromthecombinedstream,weareactuallyreadingfromthelaststreamofthepipeline
AcombinedstreamisusuallyaDuplexstream,whichisbuiltbyconnectingthefirststreamtoitsWritablesideandthelaststreamtoitsReadableside.
TipTocreateaDuplexstreamoutoftwodifferentstreams,oneWritableandone
Readable,wecanuseannpmmodulesuchasduplexer2(https://npmjs.org/package/duplexer2).
Butthat'snotenough;infact,anotherimportantcharacteristicofacombinedstreamisthatithastocapturealltheerrorsthatareemittedfromanystreaminsidethepipeline.Aswealreadymentioned,anyerroreventisnotautomaticallypropagateddownthepipeline;so,ifwewanttohavepropererrormanagement(andweshould),wewillhavetoexplicitlyattachanerrorlistenertoeachstream.However,ifthecombinedstreamisreallyablackbox,thismeansthatwedon'thaveaccesstoanyofthestreamsinthemiddleofthepipeline;soit'scrucialforthecombinedstreamtoalsoactasanaggregatorforalltheerrorscomingfromanystreaminthepipeline.
Torecap,acombinedstreamhastwomajoradvantages:
Wecanredistributeitasablackbox,byhidingitsinternalpipelineWehavesimplifiederrormanagementaswedon'thavetoattachanerrorlistenertoeachstreaminthepipeline,butjusttothecombinedstreamitself
Combiningstreamsisaprettygenericandcommonpractice,soifwedon'thaveanyparticularneedwemightjustwanttoreuseanexistingsolutionsuchasmultipipe(https://www.npmjs.org/package/multipipe)orcombine-stream(https://www.npmjs.org/package/combine-stream),justtonameafew.
ImplementingacombinedstreamTomakeasimpleexample,let'sconsiderthecaseofthefollowingtwotransformstreams:
Onethatbothcompressesandencryptsthedata.Onethatbothdecompressesanddecryptsthedata.
Usingalibrarysuchasmultipipe,wecaneasilybuildthesestreamsbycombiningsomeofthestreamsthatwealreadyhaveavailablefromthecorelibraries(file'combinedStreams.js'):
varzlib=require('zlib');
varcrypto=require('crypto');
varcombine=require('multipipe');
varfs=require('fs');
module.exports.compressAndEncrypt=function(password){
returncombine(
zlib.createGzip(),
crypto.createCipher('aes192',password)
);
}
module.exports.decryptAndDecompress=function(password){
returncombine(
crypto.createDecipher('aes192',password),
zlib.createGunzip()
);
}
Wecannowusethesecombinedstreams,asiftheywereblackboxes,forexample,tocreateasmallapplicationthatarchivesafile,bycompressingandencryptingit.Let'sdothatinanewmodulenamedarchive.js:
varfs=require('fs');
varcompressAndEncryptStream=
require('./combinedStreams').compressAndEncrypt;
fs.createReadStream(process.argv[3])
.pipe(compressAndEncryptStream(process.argv[2]))
.pipe(fs.createWriteStream(process.argv[3]+".gz.enc"));
Wecanfurtherimprovetheprecedingcodebybuildingacombinedstreamoutofthepipelinethatwecreated,thistimenottoobtainareusableblackboxbutonlytotakeadvantageofitsaggregatederrormanagement.Infact,aswealreadymentionedmanytimes,writingsomethingsuchasthefollowingwillonlycatchtheerrorsthatareemittedbythelaststream:
fs.createReadStream(process.argv[3])
.pipe(compressAndEncryptStream(process.argv[2]))
.pipe(fs.createWriteStream(process.argv[3]+".gz.enc"))
.on('error',function(err){
//Onlyerrorsfromthelaststream
console.log(err);
});
However,bycombiningallthestreamstogetherwecanfixtheproblemelegantly.Let'sthenrewritethe'archive.js'fileasfollows:
varcombine=require('multipipe');
varfs=require('fs');
varcompressAndEncryptStream=
require('./combinedStreams').compressAndEncrypt;
combine(
fs.createReadStream(process.argv[3]),
compressAndEncryptStream(process.argv[2]),
fs.createWriteStream(process.argv[3]+".gz.aes")
).on('error',function(err){
//thiserrormaycomefromanystreaminthepipeline
console.log(err);
});
Aswecansee,wecannowattachanerrorlistenerdirectlytothecombinedstreamanditwillreceiveanyerroreventthatisemittedbyanyofitsinternalstreams.
Now,torunthearchivemodule,simplyspecifyapasswordandafileinthecommandlinearguments:
nodearchivemypassword/path/to/a/file.txt
Withthisexample,wehaveclearlydemonstratedhowimportantitistocombinestreams;fromoneaspect,itallowsustocreatereusablecompositionsofstreamsandfromanotheritsimplifiestheerrormanagementofapipeline.
ForkingstreamsWecanperformaforkofastreambypipingasingleReadablestreamintomultipleWritablestreams.Thisisusefulwhenwewanttosendthesamedatatodifferentdestinations,forexample,twodifferentsocketsortwodifferentfiles.Itcanalsobeusedwhenwewanttoperformdifferenttransformationsonthesamedata,orwhenwewanttosplitthedatabasedonsomecriteria.Thefollowingfiguregivesusagraphicalrepresentationofthispattern:
ForkingastreaminNode.jsisatrivialmatter;let'sseewhybyworkingonanexample.
ImplementingamultiplechecksumgeneratorLet'screateasmallutilitythatoutputsboththesha1andmd5hashesofagivenfile.Let'scallthisnewmodulegenerateHashes.jsandlet'sstartbyinitializingourchecksumstreams:
varfs=require('fs');
varcrypto=require('crypto');
varsha1Stream=crypto.createHash('sha1');
sha1Stream.setEncoding('base64');
varmd5Stream=crypto.createHash('md5');
md5Stream.setEncoding('base64');
Nothingspecialsofar;thenextpartofthemoduleisactuallywherewewillcreateaReadablestreamfromafileandforkittotwodifferentstreamsinordertoobtaintwootherfiles,onecontainingthesha1hashandtheothercontainingthemd5checksum:
varinputFile=process.argv[2];
varinputStream=fs.createReadStream(inputFile);
inputStream
.pipe(sha1Stream)
.pipe(fs.createWriteStream(inputFile+'.sha1'));
inputStream
.pipe(md5Stream)
.pipe(fs.createWriteStream(inputFile+'.md5'));
Verysimple,right?TheinputStreamvariableispipedintosha1Streamononesideandmd5Streamontheother.Thereareacoupleofthingstonote,though,thathappenbehindthescenes:
Bothmd5Streamandsha1StreamwillbeendedautomaticallywheninputStreamends,unlesswespecify{end:false}asanoptionwheninvokingpipe()Thetwoforksofthestreamwillreceivethesamedatachunks,sowemustbeverycarefulwhenperformingside-effectoperationsonthedata,asthatwouldaffecteverystreamthatweareforkingtoBack-pressurewillworkout-of-the-box;theflowcomingfrominputStreamwillgoasfastastheslowestbranchofthefork!
MergingstreamsMergingistheoppositeoperationtoforkingandconsistsofpipingasetofReadablestreamsintoasingleWritablestream,asshowninthefollowingfigure:
Mergingmultiplestreamsintooneisingeneralasimpleoperation;however,wehavetopayattentiontothewaywehandletheendevent,aspipingusingtheautoendoptionwillcausethedestinationstreamtobeendedassoonasoneofthesourcesends.Thiscanoftenleadtoanerrorsituation,astheotheractivesourceswillstillcontinuetowritetoanalreadyterminatedstream.Thesolutiontothisproblemistousetheoption{end:false}whenpipingmultiplesourcestoasingledestinationandtheninvokeend()onthedestinationonlywhenallthesourceshavecompletedreading.
CreatingatarballfrommultipledirectoriesTomakeasimpleexample,let'simplementasmallprogramthatcreatesatarballfromthecontentsoftwodifferentdirectories.Forthispurpose,wearegoingtointroducetwonewnpmpackages:
tar(https://npmjs.org/package/tar):astreaminglibrarytocreatetarballsfstream(https://npmjs.org/package/fstream):alibrarytocreateobjectstreamsfromfilesystemfiles
OurnewmoduleisgoingtobecalledmergeTar.js;let'sdefineitscontentsstartingfromsomeinitializationsteps:
vartar=require('tar');
varfstream=require('fstream');
varpath=require('path');
vardestination=path.resolve(process.argv[2]);
varsourceA=path.resolve(process.argv[3]);
varsourceB=path.resolve(process.argv[4]);
Intheprecedingcode,wearejustloadingallthedependenciesandinitializingthevariablesthatcontainthenameofthedestinationfileandthetwosourcedirectories(sourceAandsourceB).
Next,wewillcreatethetarstreamandpipeitintoitsdestination:
varpack=tar.Pack();
pack.pipe(fstream.Writer(destination));
Nowit'stimetoinitializethesourcestreams:
varendCount=0;
functiononEnd(){
if(++endCount===2){
pack.end();
}
}
varsourceStreamA=
fstream.Reader({type:"Directory",path:sourceA})
.on('end',onEnd);
varsourceStreamB=
fstream.Reader({type:"Directory",path:sourceB})
.on('end',onEnd);
Intheprecedingcode,wecreatedthestreamsthatreadfromboththetwosourcedirectories(sourceStreamAandsourceStreamB);thenforeachsourcestreamweattachanendlistener,whichwillterminatethepackstreamonlywhenboththedirectoriesarereadcompletely.
Finally,itistimetoperformtherealmerge:
sourceStreamA.pipe(pack,{end:false});
sourceStreamB.pipe(pack,{end:false});
Wepipeboththesourcesintothepackstreamandtakecaretodisabletheautoendingofthedestinationstreambyprovidingtheoption{end:false}tothetwopipe()invocations.
Withthis,wecompletedoursimpletarutility.Wecantrythisutilitybyprovidingthedestinationfileasthefirstcommandlineargument,followedbythetwosourcedirectories:
nodemergeTardest.tar/path/to/sourceA/path/to/sourceB
Toconcludethissection,it'sworthmentioningthat,onnpm,wecanfindafewmodulesthatcansimplifythemergingofstreams,forexample:
merge-stream(https://npmjs.org/package/merge-stream)multistream-merge(https://npmjs.org/package/multistream-merge)
Asforthelastcommentonthestreammergepattern,it'sworthremindingthatthedatapipedintothedestinationstreamisrandomlyintermingled;thisisapropertythatcanbeacceptableinsometypesofobjectstreams(aswesawinthelastexample)butitisoftenanundesiredeffectwhendealingwithbinarystreams.
However,thereisonevariationofthepatternthatallowsustomergestreamsinorder;itconsistsofconsumingthesourcestreamsoneaftertheother,whenthepreviousoneends,thenextonestartsemittingchunks(itislikeconcatenatingtheoutputofallthesources).Asalways,onnpmwecanfindsomepackagesthatalsodealwiththissituation,oneofthemismultistream(https://npmjs.org/package/multistream).
MultiplexinganddemultiplexingThereisaparticularvariationofthemergestreampatterninwhichwedon'treallywanttojustjoinmultiplestreamstogetherbut,instead,touseasharedchanneltodeliverthedataofasetofstreams.Thisisaconceptuallydifferentoperationbecausethesourcestreamsremainlogicallyseparatedinsidethesharedchannel,whichallowsustosplitthestreamagainoncethedatareachestheotherendofthesharedchannel.Thefollowingfigureclarifiesthesituation:
Theoperationofcombiningmultiplestreamstogether(inthiscasealsoknownaschannels)toallowtransmissionoverasinglestreamiscalledmultiplexing,whiletheoppositeoperation,namelyreconstructingtheoriginalstreamsfromthedatareceivedfromasharedstream,iscalleddemultiplexing.Thedevicesthatperformtheseoperationsarecalledmultiplexer(ormux)anddemultiplexer(ordemux)respectively.ThisisawidelystudiedareainComputerScienceandTelecommunicationsingeneral,asitisoneofthefoundationsofalmostanytypeofcommunicationmediasuchastelephony,radio,TV,andofcoursetheInternetitself.Forthescopeofthisbook,wewillnotgotoofarwiththeexplanations,asthisisavasttopic.
Whatwewanttodemonstrateinthissection,instead,ishowit'spossibletousea
sharedNode.jsstreaminordertoconveymultiplelogicallyseparatedstreamsthatarethensplitagainattheotherendofthesharedstream.
BuildingaremoteloggerLet'suseanexampletodriveourdiscussion.Wewanttohaveasmallprogramthatstartsachildprocessandredirectsbothitsstandardoutputandstandarderrortoaremoteserver,whichinturnsavesthetwostreamsintotwoseparatefiles.So,inthiscasethesharedmediumisaTCPconnection,whilethetwochannelstobemultiplexedarethestdoutandstderrofachildprocess.Wewillleverageatechniquecalledpacketswitching,thesametechniquethatisusedbyprotocolssuchasIP,TCPorUDPandthatconsistsofwrappingthedataintopacketsallowingustospecifyvariousmetainformation,usefulformutiplexing,routing,controllingtheflow,checkingforcorrupteddata,andsoon.Theprotocolthatwearegoingtoimplementforourexampleisveryminimalist,infact,wewillsimplywrapourdataintopacketshavingthefollowingstructure:
Asshownintheprecedingfigure,thepacketcontainstheactualdata,butalsoaheader(ChannelID+Datalength),whichwillmakeitpossibletodifferentiatethedataofeachstreamandenablethedemultiplexertoroutethepackettotherightchannel.
Clientside–Multiplexing
Let'sstarttobuildourapplicationfromtheclientside.Withalotofcreativity,wewillcallthemoduleclient.js;thisrepresentsthepartoftheapplicationthatisresponsibleforstartingachildprocessandmultiplexingitsstreams.
So,let'sstartbydefiningthemodule.First,weneedsomedependencies:
varchild_process=require('child_process');
varnet=require('net');
varpath=require('path');
Then,let'simplementafunctionthatperformsthemultiplexingofalistofsources:
functionmultiplexChannels(sources,destination){
vartotalChannels=sources.length;
for(vari=0;i<sources.length;i++){
sources[i]
.on('readable',function(i){//[1]
varchunk;
while((chunk=this.read())!==null){
varoutBuff=newBuffer(1+4+chunk.length);
//[2]
outBuff.writeUInt8(i,0);
outBuff.writeUInt32BE(chunk.length,1);
chunk.copy(outBuff,5);
console.log('Sendingpackettochannel:'+i);
destination.write(outBuff);//[3]
}
}.bind(sources[i],i))
.on('end',function(){//[4]
if(--totalChannels===0){
destination.end();
}
});
}
}
ThemutiplexChannels()functiontakesinasinputthesourcestreamstobemultiplexedandthedestinationchannel,andthenitperformsthefollowingsteps:
1. Foreachsourcestream,itregistersalistenerforthereadableeventwherewereadthedatafromthestreamusingthenon-flowingmode.
2. Whenachunkisread,wewrapitintoapacketthatcontainsinorder:1byte(UInt8)forthechannelID,4bytes(UInt32BE)forthepacketsize,andthentheactualdata.
3. Whenthepacketisready,wewriteitintothedestinationstream.4. Finally,weregisteralistenerfortheendeventsothatwecanterminatethe
destinationstreamwhenallthesourcestreamsareended.
TipOurprotocolistobeabletomultiplexupto8differentsourcestreamsbecauseweonlyhave1bytetoidentifythechannel.
Nowthelastpartofourclientbecomesveryeasy:
varsocket=net.connect(3000,function(){//[1]
varchild=child_process.fork(//[2]
process.argv[2],
process.argv.slice(3),
{silent:true}
);
multiplexChannels([child.stdout,child.stderr],socket);
//[3]
});
Inthislastcodefragment,weperformthefollowingoperations::
1. WecreateanewTCPclientconnectiontotheaddresslocalhost:3000.2. Westartthechildprocessbyusingthefirstcommand-lineargumentasthepath,
whileweprovidetherestoftheprocess.argvarrayasargumentsforthechildprocess.Wespecifytheoption{silent:true},sothatthechildprocessdoesnotinheritstdoutandstderroftheparent.
3. Finally,wetakestdoutandstderrofthechildprocessandwemultiplexthemintosocketusingthemutiplexChannels()function.
Serverside–demultiplexing
Nowwecantakecareofcreatingtheserversideoftheapplication(server.js),wherewedemultiplexthestreamsfromtheremoteconnectionandpipethemintotwodifferentfiles.Let'sstartbycreatingafunctioncalleddemultiplexChannel():
functiondemultiplexChannel(source,destinations){
varcurrentChannel=null;
varcurrentLength=null;
source
.on('readable',function(){//[1]
varchunk;
if(currentChannel===null){//[2]
chunk=this.read(1);
currentChannel=chunk&&chunk.readUInt8(0);
}
if(currentLength===null){//[3]
chunk=this.read(4);
currentLength=chunk&&chunk.readUInt32BE(0);
if(currentLength===null){
return;
}
}
chunk=this.read(currentLength);//[4]
if(chunk===null){
return;
}
console.log('Receivedpacketfrom:'+currentChannel);
destinations[currentChannel].write(chunk);//[5]
currentChannel=null;
currentLength=null;
})
.on('end',function(){//[6]
destinations.forEach(function(destination){
destination.end();
});
console.log('Sourcechannelclosed');
});
}
Theprecedingcodemightlookcomplicatedbutitisnot;thankstothepullnatureofNode.jsReadablestreams,wecaneasilyimplementthedemultiplexingofourlittleprotocolasfollows:
1. Westartreadingfromthestreamusingthenon-flowingmode.2. First,ifwehavenotreadthechannelIDyet,wetrytoread1bytefromthe
streamandthentransformitintoanumber.3. Thenextstepistoreadthelengthofthedata.Weneed4bytesforthat,soit's
possible(evenifunlikely)thatwedon'thaveenoughdataintheinternalbuffer,whichwillcausethethis.read()invocationtoreturnnull.Insuchacase,wesimplyinterrupttheparsingandretryatthenextreadableevent.
4. Whenwefinallycanalsoreadthedatasize,weknowhowmuchdatatopullfromtheinternalbuffer,sowetrytoreaditall.
5. Whenwereadallthedata,wecanwriteittotherightdestinationchannel,makingsurethatweresetthecurrentChannelandcurrentLengthvariables(thesewillbeusedtoparsethenextpacket).
6. Lastly,wemakesuretoendallthedestinationchannelswhenthesourcechannelends.
Nowthatwecandemultiplexthesourcestream,let'sputournewfunctionatwork:
net.createServer(function(socket){
varstdoutStream=fs.createWriteStream('stdout.log');
varstderrStream=fs.createWriteStream('stderr.log');
demultiplexChannel(socket,[stdoutStream,stderrStream]);
}).listen(3000,function(){
console.log('Serverstarted');
});
Intheprecedingcode,wefirststartaTCPserverontheport3000,thenforeachconnectionthatwereceive,wewillcreatetwoWritablestreamspointingtotwodifferentfiles,oneforthestandardoutputandanotherforthestandarderror;theseareourdestinationchannels.Finally,weusedemultiplexChannel()todemultiplexthesocketstreamintostdoutStreamandstderrStream.
Runningthemux/demuxapplication
Now,wearereadytotryournewmux/demuxapplication,butfirstlet'screateasmall
Node.jsprogramtoproducesomesampleoutput;let'scallitgenerateData.js:
console.log("out1");
console.log("out2");
console.error("err1");
console.log("out3");
console.error("err2");
Okay,nowwearereadytotryourremoteloggingapplication.First,let'sstarttheserver:
nodeserver
Thentheclient,byprovidingthefilethatwewanttostartaschildprocess:
nodeclientgenerateData.js
Theclientwillrunalmostimmediately,butattheendoftheprocessthestandardinputandstandardoutputofthegenerateDataapplicationhavetraveledthroughonesingleTCPconnectionandthen,ontheserver,havebeendemultiplexedintotwoseparatefiles.
NotePleasemakeanotethat,asweareusingchild_process.fork()(http://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options),ourclientwillbeabletolaunchonlyotherNode.jsmodules.
MultiplexinganddemultiplexingobjectstreamsTheexamplethatwehavejustshowndemonstratedhowtomultiplexanddemultiplexabinary/textstream,butit'sworthmentioningthatthesamerulesapplyalsotoobjectstreams.Thegreatestdifferenceisthat,usingobjects,wealreadyhaveawaytotransmitthedatausingatomicmessages(theobjects),somultiplexingwouldbeaseasyassettingapropertychannelIDintoeachobject,whiledemultiplexingwouldsimplyinvolvereadingthechannelIDpropertyandroutingeachobjecttowardstherightdestinationstream.
Anotherpatterninvolvingonlydemultiplexingconsistsinroutingthedatacomingfromasourcedependingonsomecondition.Withthispattern,wecanimplementcomplexflows,suchastheoneshowninthefollowingdiagram:
Thedemultiplexerusedinthesystemdescribedbytheprecedingdiagram,takesastreamofobjectsrepresentinganimalsanddistributeseachofthemtotherightdestinationstreambasedontheclassoftheanimal:reptiles,amphibians,andmammals.
Usingthesameprinciple,wecanalsoimplementanif-elsestatementforstreams;forsomeinspiration,takealookattheternary-streampackage(https://npmjs.org/package/ternary-stream)thatallowsustodoexactlythat.
SummaryInthischapter,wehaveshedsomelightonNode.jsstreamsandtheirusecase,butatthesametimethisshouldhavethrownopenadoortoaprogrammingparadigmwithvirtuallyunlimitedpossibilities.WelearnedwhystreamsaresoacclaimedbytheNode.jscommunityandwemasteredtheirbasicfunctionality,enablingustodiscovermoreandnavigatecomfortablyinthisnewworld.Weanalyzedsomeadvancedpatternsandstartedtounderstandhowtoconnectstreamstogetherindifferentconfigurations,graspingtheimportanceofinteroperabilitywhichiswhatmakesstreamssoversatileandpowerful.
Ifwecan'tdosomethingwithonestream,weprobablycandoitbyconnectingotherstreamstogether,andthisworksgreatwiththeonethingpermodulephilosophy.Atthispoint,itshouldbeclearthatstreamsarenotjustagoodtoknowfeatureofNode.js;theyare,instead,anessentialpartofthis,acrucialpatterntohandlebinarydata,strings,andobjects.It'snotbychancethatanentirechapterwasdedicatedtothem.
Inthenextchapter,wewillfocusonthetraditionalobject-orienteddesignpatterns.Butdon'tbefooled;eventhoughJavaScriptisanobject-orientedlanguage,inNode.jsthefunctionalorhybridapproachisoftenpreferred.Getridofeveryprejudicebeforereadingthenextchapter.
Chapter4.DesignPatternsAdesignpatternisareusablesolutiontoarecurringproblem;thetermisreallybroadinitsdefinitionandcanspanmultipledomainsofapplication.However,thetermisoftenassociatedwithawell-knownsetofobject-orientedpatternsthatwerepopularizedinthe90'sbythebook,DesignPatterns:ElementsofReusableObject-OrientedSoftware,PearsonEducationbythealmostlegendaryGangofFour(GoF):ErichGamma,RichardHelm,RalphJohnson,andJohnVlissides.Wewilloftenrefertothesespecificsetofpatternsastraditionaldesignpatterns,orGoFdesignpatterns.
Applyingthissetofobject-orienteddesignpatternsinJavaScriptisnotaslinearandformalasitwouldhappeninaclass-basedobject-orientedlanguage.Asweknow,JavaScriptismulti-paradigm,object-oriented,andprototype-based,andhasdynamictyping;ittreatsfunctionsasfirstclasscitizens,andallowsfunctionalprogrammingstyles.ThesecharacteristicsmakeJavaScriptaveryversatilelanguage,whichgivestremendouspowertothedeveloper,butatthesametime,itcausesafragmentationofprogrammingstyles,conventions,techniques,andultimatelythepatternsofitsecosystem.TherearesomanywaystoachievethesameresultusingJavaScriptthateverybodyhastheirownopiniononwhatthebestwayistoapproachaproblem.AcleardemonstrationofthisphenomenonistheabundanceofframeworksandopinionatedlibrariesintheJavaScriptecosystem;probably,nootherlanguagehaseverseensomany,especiallynowthatNode.jshasgivennewastonishingpossibilitiestoJavaScriptandhascreatedsomanynewscenarios.
Inthiscontext,thetraditionaldesignpatternstooareaffectedbythenatureofJavaScript.Therearesomanywaysinwhichtheycanbeimplementedsothattheirtraditional,stronglyobject-orientedimplementationisnotapatternanymore,andinsomecases,notevenpossiblebecauseJavaScript,asweknow,doesn'thaverealclassesorabstractinterfaces.Whatdoesn'tchangethough,istheoriginalideaatthebaseofeachpattern,theproblemitsolves,andtheconceptsattheheartofthesolution.
Inthischapter,wewillseehowsomeofthemostimportantGoFdesignpatternsapplytoNode.jsanditsphilosophy,thusrediscoveringtheirimportancefromanotherperspective.
Thedesignpatternsexploredinthischapterareasfollows:
FactoryProxyDecorator
AdapterStrategyStateTemplateMiddlewareCommand
NoteThischapterassumesthatthereaderhassomenotionofhowinheritanceworksinJavaScript.PleasealsobeadvisedthatthroughoutthischapterwewilloftenusegenericandmoreintuitivediagramstodescribeapatterninplaceofstandardUML,sincemanypatternscanhaveanimplementationbasednotonlyonclasses,butalsoonobjectsandevenfunctions.
FactoryWebeginourjourneystartingfromwhatprobablyisthemostsimpleandcommondesignpatterninNode.js:Factory.
AgenericinterfaceforcreatingobjectsWealreadystressedthefactthat,inJavaScript,thefunctionalparadigmisoftenpreferredtoapurelyobject-orienteddesign,foritssimplicity,usability,andsmallsurfacearea.Thisisespeciallytruewhencreatingnewobjectinstances.Infact,invokingafactory,insteadofdirectlycreatinganewobjectfromaprototypeusingthenewoperatororObject.create(),issomuchmoreconvenientandflexibleunderseveralaspects.
Firstandforemost,afactoryallowsustoseparatetheobjectcreationfromitsimplementation;essentially,afactorywrapsthecreationofanewinstance,givingusmoreflexibilityandcontrolinthewaywedoit.Insidethefactory,wecancreateanewinstanceleveragingclosures,usingaprototypeandthenewoperator,usingObject.create(),orevenreturningadifferentinstancebasedonaparticularcondition.Theconsumerofthefactoryistotallyagnosticabouthowthecreationoftheinstanceiscarriedout.Thetruthisthat,byusingnew,wearebindingourcodetoonespecificwayofcreatinganobject,whileinJavaScriptwecanhavemuchmoreflexibility,almostforfree.Asaquickexample,let'sconsiderasimplefactorythatcreatesanImageobject:
functioncreateImage(name){
returnnewImage(name);
}
varimage=createImage('photo.jpeg');
ThecreateImage()factorymightlooktotallyunnecessary,whynotinstantiatetheImageclassbyusingthenewoperatordirectly?Somethinglikethefollowinglineofcode:
varimage=newImage(name);
Aswealreadymentioned,usingnewbindsourcodetooneparticulartypeofobject;intheprecedingcase,toobjectsoftype,Image.Afactoryinstead,givesusmuchmoreflexibility;imaginethatwewanttorefactortheImageclass,splittingitintosmallerclasses,oneforeachimageformatthatwesupport.Ifweexposedafactoryastheonlymeanstocreatenewimages,wecansimplyrewriteitasfollows,withoutbreakinganyoftheexistingcode:
functioncreateImage(name){
if(name.match(/\.jpeg$/)){
returnnewJpegImage(name);
}elseif(name.match(/\.gif$/)){
returnnewGifImage(name);
}elseif(name.match(/\.png$/)){
returnnewPngImage(name);
}else{
thrownewException('Unsupportedformat');
}
}
Ourfactoryalsoallowsustonotexposetheconstructorsoftheobjectsitcreates,andpreventsthemfrombeingextendedormodified(remembertheprincipleofsmallsurfacearea?).InNode.js,thiscanbeachievedbyexportingonlythefactory,whilekeepingeachconstructorprivate.
AmechanismtoenforceencapsulationAfactorycanalsobeusedasanencapsulationmechanism,thankstoclosures.
NoteEncapsulationreferstothetechniqueofcontrollingtheaccesstosomeinternaldetailsofanobjectbypreventingtheexternalcodefrommanipulatingthemdirectly.Theinteractionwiththeobjecthappensonlythroughitspublicinterface,
isolatingtheexternalcodefromthechangesintheimplementationdetailsoftheobject.Thispracticeisalsoreferredtoasinformationhiding.Encapsulationisalsoafundamentalprincipleofobject-orienteddesign,togetherwithinheritance,polymorphism,andabstraction.
Asweknow,inJavaScript,wedon'thaveaccesslevelmodifiers(forexample,wecan'tdeclareaprivatevariable),sotheonlywaytoenforceencapsulationisthroughfunctionscopesandclosures.Afactorymakesitstraightforwardtoenforceprivatevariables,considerthefollowingcodeforexample:
functioncreatePerson(name){
varprivateProperties={};
varperson={
setName:function(name){
if(!name)thrownewError('Apersonmusthaveaname');
privateProperties.name=name;
},
getName:function(){
returnprivateProperties.name;
}
};
person.setName(name);
returnperson;
}
Intheprecedingcode,weleverageclosurestocreatetwoobjects:apersonobjectwhichrepresentsthepublicinterfacereturnedbythefactoryandagroupofprivatePropertiesthatareinaccessiblefromtheoutside,andthatcanbemanipulatedonlythroughtheinterfaceprovidedbythepersonobject.Forexample,intheprecedingcode,wemakesurethataperson'snameisneverempty,thiswouldnotbepossibletoenforceifnamewasjustapropertyofthepersonobject.
NoteFactoriesareonlyoneofthetechniquesthatwehaveforcreatingprivatemembers;infact,otherpossibleapproachesareasfollows:
Definingprivatevariablesinaconstructor(asrecommendedbyDouglasCrockford:http://javascript.crockford.com/private.html)Usingconventions,forexample,prefixingthenameofapropertywithanunderscore"_"orthedollarsign"$"(thishowever,doesnottechnicallypreventamemberfrombeingaccessedfromtheoutside)UsingES6WeakMaps(http://fitzgeraldnick.com/weblog/53/)
BuildingasimplecodeprofilerNow,let'sworkonacompleteexampleusingafactory.Let'sbuildasimplecodeprofiler,anobjectwiththefollowingproperties:
Astart()methodthattriggersthestartofaprofilingsessionAnend()methodtoterminatethesessionandlogitsexecutiontimetotheconsole
Let'sstartbycreatingafilenamedprofiler.js,whichwillhavethefollowingcontent:
functionProfiler(label){
this.label=label;
this.lastTime=null;
}
Profiler.prototype.start=function(){
this.lastTime=process.hrtime();
}
Profiler.prototype.end=function(){
vardiff=process.hrtime(this.lastTime);
console.log('Timer"'+this.label+'"took'
+diff[0]+'secondsand'
+diff[1]+'nanoseconds.');
}
Thereisnothingfancyintheprecedingclass;wesimplyusethedefaulthighresolutiontimertosavethecurrenttimewhenstart()isinvoked,andthencalculatetheelapsedtimewhenend()isexecuted,printingtheresulttotheconsole.
Now,ifwearegoingtousesuchaprofilerinareal-worldapplicationtocalculatetheexecutiontimeofthedifferentroutines,wecaneasilyimaginethehugeamountofloggingwewillgeneratetothestandardoutput,especiallyinaproductionenvironment.Whatwemightwanttodoinsteadisredirecttheprofilinginformationtoanothersource,forexample,adatabase,oralternatively,disablingtheprofileraltogetheriftheapplicationisrunninginproductionmode.It'sclearthatifweweretoinstantiateaProfilerobjectdirectlybyusingthenewoperator,wewouldneedsomeextralogicintheclientcodeorintheProfilerobjectitselfinordertoswitchbetweenthedifferentlogics.WecaninsteaduseafactorytoabstractthecreationoftheProfilerobject,sothat,dependingonwhethertheapplicationrunsinproductionordevelopmentmode,wecanreturnafullyworkingProfilerobject,oralternatively,amockobjectwiththesameinterface,butwithemptymethods.Let'sdothisthen,in
theprofiler.jsmoduleinsteadofexportingtheProfilerconstructor,wewillexportonlyafunction,ourfactory.Thefollowingisitscode:
module.exports=function(label){
if(process.env.NODE_ENV==='development'){
returnnewProfiler(label);//[1]
}elseif(process.env.NODE_ENV==='production'){
return{//[2]
start:function(){},
end:function(){}
}
}else{
thrownewError('MustsetNODE_ENV');
}
}
Thefactorythatwecreatedabstractsthecreationofaprofilerobjectfromitsimplementation:
Iftheapplicationisrunningindevelopmentmode,wereturnanew,fullyfunctionalProfilerobjectIfinsteadtheapplicationisrunninginproductionmode,wereturnamockobjectwherethestart()andstop()methodsareemptyfunctions
Thenicefeaturetohighlightisthat,thankstotheJavaScriptdynamictyping,wewereabletoreturnanobjectinstantiatedwiththenewoperatorinonecircumstanceandasimpleobjectliteralintheother(thisisalsoknownasducktypinghttp://en.wikipedia.org/wiki/Duck_typing).Ourfactoryisdoingitsjobperfectly;wecanreallycreateobjectsinanywaythatwelikeinsidethefactoryfunction,andwecanexecuteadditionalinitializationstepsorreturnadifferenttypeofobjectbasedonparticularconditions,andallofthiswhileisolatingtheconsumeroftheobjectfromallthesedetails.Wecaneasilyunderstandthepowerofthissimplepattern.
Now,wecanplaywithourprofiler;thisisapossibleusecaseforthefactorythatwejustcreatedearlier:
varprofiler=require('./profiler');
functiongetRandomArray(len){
varp=profiler('Generatinga'+len+'itemslong
array');
p.start();
vararr=[];
for(vari=0;i<len;i++){
arr.push(Math.random());
}
p.end();
}
getRandomArray(1e6);
console.log('Done');
ThepvariablecontainstheinstanceofourProfilerobject,butwedon'tknowhowit'screatedandwhatitsimplementationisatthispointinthecode.
IfweincludetheprecedingcodeinafilenamedprofilerTest.js,wecaneasilytesttheseassumptions.Totrytheprogramwithprofilingenabled,runthefollowingcommand:
exportNODE_ENV=development;nodeprofilerTest
Theprecedingcommandenablestherealprofilerandprintstheprofilinginformationtotheconsole.Ifwewanttotrythemockprofilerinstead,wecanrunthefollowingcommand:
exportNODE_ENV=production;nodeprofilerTest
Theexamplethatwejustpresentedisjustasimpleapplicationofthefactoryfunctionpattern,butitclearlyshowstheadvantagesofseparatinganobject'screationfromitsimplementation.
InthewildAswesaid,factoriesareverypopularinNode.js.Manypackagesofferonlyafactoryforcreatingnewinstances,someexamplesarethefollowing:
Dnode(https://npmjs.org/package/dnode):ThisisanRPCsystemforNode.js.Ifwelookintoitssourcecode,wewillseethatitslogicisimplementedintoaclassnamedD;however,thisisneverexposedtotheoutsideastheonlyexportedinterfaceisafactory,whichallowsustocreatenewinstancesoftheclass.Youcantakealookatitssourcecodeathttps://github.com/substack/dnode/blob/34d1c9aa9696f13bdf8fb99d9d039367ad873f90/index.js#L7-9.Restify(https://npmjs.org/package/restify):ThisisaframeworktobuildRESTAPIthatallowsustocreatenewinstancesofaserverusingtherestify.createServer()factory,whichinternallycreatesanewinstanceoftheServerclass(whichisnotexported).Youcantakealookatitssourcecodeathttps://github.com/mcavage/node-restify/blob/5f31e2334b38361ac7ac1a5e5d852b7206ef7d94/lib/index.js#L91-116.
Othermodulesexposebothaclassandafactory,butdocumentthefactoryasthemainmethod—orthemostconvenientway—tocreatenewinstances;someoftheexamplesareasfollows:
http-proxy(https://npmjs.org/package/http-proxy):Thisisaprogrammableproxyinglibrary,wherenewinstancesarecreatedwithhttpProxy.createProxyServer(options).ThecoreNode.jsHTTPserver:Thisiswherenewinstancesaremostlycreatedusinghttp.createServer(),eventhoughthisisessentiallyashortcutfornewhttp.Server().bunyan(https://npmjs.org/package/bunyan):Thisisapopularlogginglibrary;initsreadmefilethecontributorsproposeafactory,bunyan.createLogger(),asthemainmethodtocreatenewinstances,eventhoughthiswouldbeequivalenttorunningnewbunyan().
Someothermodulesprovideafactorytowrapthecreationofothercomponents.Popularexamplesarethrough2andfrom2(wesawtheminChapter3,CodingwithStreams),whichallowustosimplifythecreationofnewstreamsusingafactoryapproach,freeingthedeveloperfromexplicitlyusinginheritanceandthenewoperator.
ProxyAproxyisanobjectthatcontrolstheaccesstoanotherobjectcalledsubject.Theproxyandthesubjecthaveanidenticalinterfaceandthisallowsustotransparentlyswaponefortheother;infact,thealternativenameforthispatternissurrogate.Aproxyinterceptsallorsomeoftheoperationsthataremeanttobeexecutedonthesubject,augmentingorcomplementingtheirbehavior.Thefollowingfigureshowsthediagrammaticrepresentation:
TheprecedingfigureshowsushowtheProxyandtheSubjecthavethesameinterfaceandhowthisistotallytransparenttotheclient,whocanuseoneortheotherinterchangeably.TheProxyforwardseachoperationtothesubject,enhancingitsbehaviorwithadditionalpreprocessingorpost-processing.
NoteIt'simportanttoobservethatwearenottalkingaboutproxyingbetweenclasses;theProxypatterninvolveswrappingactualinstancesofthesubject,thuspreservingitsstate.
Aproxyisusefulinseveralcircumstances,forexample,considerthefollowingones:
Datavalidation:TheproxyvalidatestheinputbeforeforwardingittothesubjectSecurity:TheproxyverifiesthattheclientisauthorizedtoperformtheoperationanditpassestherequesttothesubjectonlyiftheoutcomeofthecheckispositiveCaching:TheproxykeepsaninternalcachesothattheoperationsareexecutedonthesubjectonlyifthedataisnotyetpresentinthecacheLazyinitialization:Ifthecreationofthesubjectisexpensive,theproxycandelayittowhenit'sreallynecessaryLogging:Theproxyinterceptsthemethodinvocationsandtherelativeparameters,recodingthemastheyhappenRemoteobjects:Aproxycantakeanobjectthatislocatedremotely,andmakeitappearlocal
Ofcourse,therearemanymoreapplicationsfortheProxypattern,buttheseshouldgiveusanideaoftheextentofitspurpose.
TechniquesforimplementingproxiesWhenproxyinganobject,wecandecidetointerceptallitsmethodsoronlypartofthem,whiledelegatingtherestofthemdirectlytothesubject.Thereareseveralwaysinwhichthiscanbeachieved;let'sanalyzesomeofthem.
ObjectcompositionCompositionisthetechniquewherebyanobjectiscombinedwithanotherobjectforthepurposeofextendingorusingitsfunctionality.InthespecificcaseoftheProxypattern,anewobjectwiththesameinterfaceasthesubjectiscreated,andareferencetothesubjectisstoredinternallyintheproxyintheformofaninstancevariableoraclosurevariable.Thesubjectcanbeinjectedfromtheclientatcreationtimeorcreatedbytheproxyitself.
Thefollowingisoneexampleofthistechniqueusingapseudoclassandafactory:
functioncreateProxy(subject){
varproto=Object.getPrototypeOf(subject);
functionProxy(subject){
this.subject=subject;
}
Proxy.prototype=Object.create(proto);
//proxiedmethod
Proxy.prototype.hello=function(){
returnthis.subject.hello()+'world!';
}
//delegatedmethod
Proxy.prototype.goodbye=function(){
returnthis.subject.goodbye
.apply(this.subject,arguments);
}
returnnewProxy(subject);
}
Toimplementaproxyusingcomposition,wehavetointerceptthemethodsthatweareinterestedinmanipulating(suchashello()),whilesimplydelegatingtherestofthemtothesubject(aswedidwithgoodbye()).
Theprecedingcodealsoshowstheparticularcasewherethesubjecthasa
prototypeandwewanttomaintainthecorrectprototypechain,sothat,executingproxyinstanceofSubjectwillreturntrue;weusedpseudo-classicalinheritancetoachievethis.
Thisisjustanextrastep,requiredonlyifweareinterestedinmaintainingtheprototypechain,whichcanbeusefulinordertoimprovethecompatibilityoftheproxywithcodeinitiallymeanttoworkwiththesubject.
However,asJavaScripthasdynamictyping,mostofthetimewecanavoidusinginheritanceandusemoreimmediateapproaches.Forexample,analternativeimplementationoftheproxypresentedintheprecedingcode,mightjustuseanobjectliteralandafactory:
functioncreateProxy(subject){
return{
//proxiedmethod
hello:function(){
returnsubject.hello()+'world!';
},
//delegatedmethod
goodbye:function(){
returnsubject.goodbye.apply(subject,arguments);
}
};
}
NoteIfwewanttocreateaproxythatdelegatesmostofitsmethods,itwouldbeconvenienttogeneratetheseautomaticallyusingalibrary,suchasdelegates(https://npmjs.org/package/delegates).
ObjectaugmentationObjectaugmentation(ormonkeypatching)isprobablythemostpragmaticwayofproxyingindividualmethodsofanobjectandconsistsofmodifyingthesubjectdirectlybyreplacingamethodwithitsproxiedimplementation;considerthefollowingexample:
functioncreateProxy(subject){
varhelloOrig=subject.hello;
subject.hello=function(){
returnhelloOrig.call(this)+'world!';
}
returnsubject;
}
Thistechniqueisdefinitelythemostconvenientonewhenweneedtoproxyonlyoneorafewmethods,butithasthedrawbackofmodifyingthesubjectobjectdirectly.
AcomparisonofthedifferenttechniquesCompositioncanbeconsideredthesafestwayofcreatingaproxy,becauseitleavesthesubjectuntouchedwithoutmutatingitsoriginalbehavior.Itsonlydrawbackisthatwehavetomanuallydelegateallthemethods,evenifwewanttoproxyonlyoneofthem.Ifneeded,wemightalsohavetodelegatetheaccesstothepropertiesofthesubject.
TipTheobjectpropertiescanbedelegatedusingObject.defineProperty().Findoutmoreathttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty.
Objectaugmentation,ontheotherhand,modifiesthesubject,whichmightnotalwaysbewhatwewant,butitdoesnotpresentthevariousinconveniencesrelatedtodelegation.Forthisreason,objectaugmentationisdefinitelythemostpragmaticwaytoimplementproxiesinJavaScript,andit'sthepreferredtechniqueinallthosecircumstanceswheremodifyingthesubjectisnotabigconcern.
However,thereisatleastonesituationwherecompositionisalmostnecessary;thisiswhenwewanttocontroltheinitializationofthesubjectasforexample,tocreateitonlywhenneeded(lazyinitialization).
TipItisworthpointingoutthatbyusingafactoryfunction(createProxy()inourexamples),wecanshieldourcodefromthetechniqueusedtogeneratetheproxy.
CreatingaloggingWritablestream
Toseetheproxypatterninarealexample,wewillnowbuildanobjectthatactsasaproxytoaWritablestream,byinterceptingallthecallstothewrite()methodandloggingamessageeverytimethishappens.Wewilluseanobjectcompositiontoimplementourproxy;thisishowtheloggingWritable.jsfilelooks:
functioncreateLoggingWritable(writableOrig){
varproto=Object.getPrototypeOf(writableOrig);
functionLoggingWritable(subject){
this.writableOrig=writableOrig;
}
LoggingWritable.prototype=Object.create(proto);
LoggingWritable.prototype.write=
function(chunk,encoding,callback){
if(!callback&&typeofencoding==='function'){
callback=encoding;
encoding=undefined;
}
console.log('Writing',chunk);
returnthis.writableOrig.write(chunk,encoding,
function(){
console.log('Finishedwriting',chunk);
callback&&callback();
});
};
LoggingWritable.prototype.on=function(){
returnthis.writableOrig.on
.apply(this.writableOrig,arguments);
};
LoggingWritable.prototype.end=function(){
returnthis.writableOrig.end
.apply(this.writableOrig,arguments);
}
returnnewLoggingWritable(this.writableOrig);
}
Intheprecedingcode,wecreatedafactorythatreturnsaproxiedversionofthewritableobjectpassedasanargument.Weprovideanoverrideforthewrite()methodthatlogsamessagetothestandardoutputeverytimeitisinvokedandeverytimetheasynchronousoperationcompletes.Thisisalsoagoodexampletodemonstratetheparticularcaseofcreatingproxiesofasynchronousfunctions,whichmakesnecessarytoproxythecallbackaswell;thisisanimportantdetailtobeconsideredinaplatformlikeNode.js.Theremainingmethods,on()andend(),aresimplydelegatedtotheoriginalwritablestream(tokeepthecodeleanerwearenotconsideringtheothermethodsoftheWritableinterface).
WecannowincludeafewmorelinesofcodeintothelogginWritable.jsmoduletotesttheproxythatwejustcreated:
varfs=require('fs');
varwritable=fs.createWriteStream('test.txt');
varwritableProxy=createProxy(writable);
writableProxy.write('Firstchunk');
writableProxy.write('Secondchunk');
writable.write('Thisisnotlogged');
writableProxy.end();
Theproxydidnotchangetheoriginalinterfaceofthestreamoritsexternalbehavior,butifweruntheprecedingcode,wewillnowseethateverychunkthatiswrittenintothestreamistransparentlyloggedtotheconsole.
Proxyintheecosystem–functionhooksandAOPInitsnumerousforms,ProxyisaquitepopularpatterninNode.jsandintheecosystem.Infact,wecanfindseverallibrariesthatallowustosimplifythecreationofproxies,mostofthetimeleveragingobjectaugmentationasanimplementationapproach.Inthecommunity,thispatterncanbealsoreferredtoasfunctionhookingorsometimesalsoasAspectOrientedProgramming(AOP),whichisactuallyacommonareaofapplicationforproxies.AsithappensinAOP,theselibrariesusuallyallowthedevelopertosetpreorpostexecutionhooksforaspecificmethod(orasetofmethods)thatallowustoexecutesomecustomcodebeforeandaftertheexecutionoftheadvisedmethodrespectively.
Sometimesproxiesarealsocalledmiddleware,because,asithappensinthemiddlewarepattern(whichwewillseelaterinthechapter),theyallowustopreprocessandpost-processtheinput/outputofafunction.Sometimes,theyalsoallowtoregistermultiplehooksforthesamemethodusingamiddleware-likepipeline.
Thereareseverallibrariesonnpmthatallowustoimplementfunctionhookswithlittleeffort.Amongthemtherearehooks(https://npmjs.org/package/hooks),hooker(https://npmjs.org/package/hooker),andmeld(https://npmjs.org/package/meld).
InthewildMongoose(http://mongoosejs.com)isapopularObject-DocumentMapping(ODM)libraryforMongoDB.Internally,itusesthehookspackage(https://npmjs.org/package/hooks)toprovidepreandpostexecutionhooksfortheinit,validate,save,andremovemethodsofitsDocumentobjects.Findoutmoreon
theofficialdocumentationathttp://mongoosejs.com/docs/middleware.html.
DecoratorDecoratorisastructuralpatternthatconsistsofdynamicallyaugmentingthebehaviorofanexistingobject.It'sdifferentfromclassicalinheritance,becausethebehaviorisnotaddedtoalltheobjectsofthesameclassbutonlytotheinstancesthatareexplicitlydecorated.
Implementation-wise,itisverysimilartotheProxypattern,butinsteadofenhancingormodifyingthebehavioroftheexistinginterfaceofanobject,itaugmentsitwithnewfunctionalities,asdescribedinthefollowingfigure:
Inthepreviousfigure,theDecoratorobjectisextendingtheComponentobjectbyaddingthemethodC()operation.Theexistingmethodsareusuallydelegatedtothedecoratedobject,withoutfurtherprocessing.Ofcourse,ifnecessarywecaneasilycombinetheProxypattern,sothatalsothecallstotheexistingmethodscanbeinterceptedandmanipulated.
TechniquesforimplementingdecoratorsAlthoughProxyandDecoratorareconceptuallytwodifferentpatterns,withdifferentintents,theypracticallysharethesameimplementationstrategies.Let'srevisethem.
CompositionUsingcomposition,thedecoratedcomponentiswrappedaroundanewobjectthatusuallyinheritsfromit.TheDecoratorinthiscasesimplyneedstodefinethenewmethodswhiledelegatingtheexistingonestotheoriginalcomponent:
functiondecorate(component){
varproto=Object.getPrototypeOf(component);
functionDecorator(component){
this.component=component;
}
Decorator.prototype=Object.create(proto);
//newmethod
Decorator.prototype.greetings=function(){
//...
};
//delegatedmethod
Decorator.prototype.hello=function(){
this.component.hello.apply(this.component,arguments);
};
returnnewDecorator(component);
}
ObjectaugmentationObjectdecorationcanalsobeachievedbysimplyattachingnewmethodsdirectlytothedecoratedobject,asfollows:
functiondecorate(component){
//newmethod
component.greetings=function(){
//...
};
returncomponent;
}
ThesamecaveatsdiscussedduringtheanalysisoftheProxypatternarealsovalidforDecorator.Let'snowpracticethepatternwithaworkingexample!
DecoratingaLevelUPdatabaseBeforewestartcodingwiththenextexample,let'sspendafewwordstointroduceLevelUP,themodulethatwearenowgoingtoworkwith.
IntroducingLevelUPandLevelDBLevelUP(https://npmjs.org/package/levelup)isaNode.jswrapperaroundGoogle'sLevelDB,akey-valuestoreoriginallybuilttoimplementIndexedDBintheChromebrowser,butit'smuchmorethanthat.LevelDBhasbeendefinedbyDominicTarrasthe"Node.jsofdatabases",becauseofitsminimalismandextensibility.LikeNode.js,LevelDBprovidesblazingfastperformancesandonlythemostbasicsetoffeatures,allowingdeveloperstobuildanykindofdatabaseontopofit.
TheNode.jscommunity,andinthiscaseRodVagg,didnotmissthechancetobringthepowerofthisdatabaseintoNode.jsbycreatingLevelUP.BornasawrapperforLevelDB,itthenevolvedtosupportseveralkindsofbackends,fromin-memorystores,tootherNoSQLdatabasessuchasRiakandRedis,towebstorageengines
suchasIndexedDBandlocalStorage,allowingtousethesameAPIonboththeserverandtheclient,openingupsomereallyinterestingscenarios.
Today,thereisafull-fledgedecosystemaroundLevelUpmadeofpluginsandmodulesthatextendthetinycoretoimplementfeaturessuchasreplication,secondaryindexes,liveupdates,queryengines,andmore.Also,completedatabaseswerebuiltontopofLevelUP,includingCouchDBclonessuchasPouchDB(https://npmjs.org/package/pouchdb)andCouchUP(https://npmjs.org/package/couchup),andevenagraphdatabase,levelgraph(https://npmjs.org/package/levelgraph)thatcanworkbothonNode.jsandthebrowser!
NoteFindoutmoreabouttheLevelUPecosystemathttps://github.com/rvagg/node-levelup/wiki/Modules.
ImplementingaLevelUPpluginInthenextexample,wearegoingtoshowhowwecancreateasimplepluginforLevelUpusingtheDecoratorpattern,andinparticular,theobjectaugmentationtechnique,whichisthesimplestbutnonethelessthemostpragmaticandeffectivewaytodecorateobjectswithadditionalcapabilities.
NoteForconvenience,wearegoingtousethelevelpackage(http://npmjs.org/package/level)thatbundlesbothlevelupandthedefaultadaptercalledleveldown,whichusesLevelDBasthebackend.
WhatwewanttobuildisapluginforLevelUPthatallowstoreceivenotificationseverytimeanobjectwithacertainpatternissavedintothedatabase.Forexample,ifwesubscribetoapatternsuchas{a:1},wewanttoreceiveanotificationwhenobjectssuchas{a:1,b:3}or{a:1,c:'x'}aresavedintothedatabase.
Let'sstarttobuildoursmallpluginbycreatinganewmodulecalledlevelSubscribe.js.Wewilltheninsertthefollowingcode:
module.exports=functionlevelSubscribe(db){
db.subscribe=function(pattern,listener){//[1]
db.on('put',function(key,val){//[2]
varmatch=Object.keys(pattern).every(function(k){
//[3]
returnpattern[k]===val[k];
});
if(match){
listener(key,val);//[4]
}
});
};
returndb;
}
That'sitforourplugin,andit'sextremelysimple.Let'sseewhathappensintheprecedingcodebriefly:
1. Wedecoratedthedbobjectwithanewmethodnamedsubscribe().Wesimplyattachedthemethoddirectlytotheprovideddbinstance(objectaugmentation).
2. Welistenforanyputoperationperformedonthedatabase.3. Weperformedaverysimplepattern-matchingalgorithm,whichverifiedthatall
thepropertiesintheprovidedpatternarealsoavailableonthedatabeinginserted.
4. Ifwehaveamatch,wenotifythelistener.
Let'snowcreatesomecode—inanewfilenamedlevelSubscribeTest.js—totryoutournewplugin:
varlevel=require('level');//[1]
vardb=level(__dirname+'/db',{valueEncoding:'json'});
varlevelSubscribe=require('./levelSubscribe');//[2]
db=levelSubscribe(db);
db.subscribe({doctype:'tweet',language:'en'},//[3]
function(k,val){
console.log(val);
});
//[4]
db.put('1',{doctype:'tweet',text:'Hi',language:'en'});
db.put('2',{doctype:'company',name:'ACMECo.'});
Thisiswhatwedidintheprecedingcode:
1. First,weinitializeourLevelUPdatabase,choosingthedirectorywherethefileswillbestoredandthedefaultencodingforthevalues.
2. Then,weattachourplugin,whichdecoratestheoriginaldbobject.3. Atthispoint,wearereadytousethenewfeatureprovidedbyourplugin,the
subscribe()method,wherewespecifythatweareinterestedinalltheobjectswithdoctype:'tweet'andlanguage:'en'.
4. Finally,wesavesomevaluesinthedatabase,sothatwecanseeourplugininaction:
db.put('1',{doctype:'tweet',text:'Hi',language:
'en'});
db.put('2',{doctype:'company',name:'ACMECo.'});
Thisexampleshowsarealapplicationofthedecoratorpatterninitsmostsimpleimplementation:objectaugmentation.Itmightlooklikeatrivialpatternbutithasundoubtedpowerifusedappropriately.
TipForsimplicity,ourpluginwillworkonlyincombinationwiththeputoperations,butitcanbeeasilyexpandedtoworkevenwiththebatchoperations(https://github.com/rvagg/node-levelup#batch).
InthewildFormoreexamplesofhowDecoratorisusedintherealworld,wemightwanttoinspectthecodeofsomemoreLevelUpplugins:
level-inverted-index(https://github.com/dominictarr/level-inverted-index):ThisisapluginthataddsinvertedindexestoaLevelUPdatabase,allowingtoperformsimpletextsearchesacrossthevaluesstoredinthedatabaselevel-plus(https://github.com/eugeneware/levelplus):ThisisapluginthataddsatomicupdatestoaLevelUPdatabase
AdapterTheAdapterpatternallowsustoaccessthefunctionalityofanobjectusingadifferentinterface.Asthenamesuggests,itadaptsanobjectsothatitcanbeusedbycomponentsexpectingadifferentinterface.Thefollowingdiagramclarifiesthesituation:
TheprecedingdiagramshowshowtheAdapterisessentiallyawrapperfortheAdaptee,exposingadifferentinterface.ThediagramalsohighlightthefactthattheoperationsoftheAdaptercanalsobeacompositionofoneormoremethodinvocationsontheAdaptee.Fromanimplementationperspective,themostcommontechniqueiscompositionwherethemethodsoftheAdapterprovidesabridgetothemethodsoftheAdaptee.Thispatternisprettystraightforwardsolet'sworkimmediatelyonanexample.
UsingLevelUPthroughthefilesystemAPIWearenowgoingtobuildanadapteraroundtheLevelUPAPI,transformingitintoaninterfacethatiscompatiblewiththecorefsmodule.Inparticular,wewillmakesurethateverycalltoreadFile()andwriteFile()willtranslateintocallstodb.get()anddb.put();thiswaywewillbeabletouseaLevelUPdatabaseasastoragebackendforsimplefilesystemoperations.
Let'sstartbycreatinganewmodulenamedfsAdapter.js.WewillbeginbyloadingthedependenciesandexportingthecreateFsAdapter()factorythatwearegoingtousetobuildtheadapter:
varpath=require('path');
module.exports=functioncreateFsAdapter(db){
varfs={};
//...continueswiththenextcodefragments
Next,wewillimplementthereadFile()functioninsidethefactoryandensurethatitsinterfaceiscompatiblewiththeoneoftheoriginalfunctionfromthefsmodule:
fs.readFile=function(filename,options,callback){
if(typeofoptions==='function'){
callback=options;
options={};
}elseif(typeofoptions==='string'){
options={encoding:options};
}
db.get(path.resolve(filename),{//[1]
valueEncoding:options.encoding
},
function(err,value){
if(err){
if(err.type==='NotFoundError'){//[2]
err=newError('ENOENT,open\''+filename+'\'');
err.code='ENOENT';
err.errno=34;
err.path=filename;
}
returncallback&&callback(err);
}
callback&&callback(null,value);//[3]
}
);
};
Intheprecedingcode,wehadtodosomeextraworktomakesurethatthebehaviorofournewfunctionisascloseaspossibletotheoriginalfs.readFile()function.Thestepsperformedbythefunctionaredescribedasfollows:
1. Toretrieveafilefromthedbclass,weinvokedb.get()usingfilenameasakey,bymakingsuretoalwaysuseitsfullpath(usingpath.resolve()).WesetthevalueofvalueEncodingusedbythedatabasetobeequaltoanyeventualencodingoptionreceivedasaninput.
2. Ifthekeyisnotfoundinthedatabase,wecreateanerrorwithENOENTaserrorcode,whichisthecodeusedbytheoriginalfsmoduletoindicateamissingfile.Anyothertypeoferrorisforwardedtocallback(forthescopeofthisexample,weareadaptingonlythemostcommonerrorcondition).
3. Ifthekey-valuepairisretrievedsuccessfullyfromthedatabase,wewillreturnthevaluetothecallerusingthecallback.
Aswesee,thefunctionthatwecreatedisquiterough;itdoesnotwanttobeaperfectreplacementforthefs.readFile()functionbutitdefinitelydoesitsjobinthemostcommonsituations.
Tocompleteoursmalladapter,let'snowseehowtoimplementthewriteFile()function:
fs.writeFile=function(filename,contents,options,callback)
{
if(typeofoptions==='function'){
callback=options;
options={};
}elseif(typeofoptions==='string'){
options={encoding:options};
}
db.put(path.resolve(filename),contents,{
valueEncoding:options.encoding
},callback);
}
Also,inthiscase,wedon'thaveaperfectwrapper,wewillignoresomeoptionssuchasfilepermissions(options.mode),andwewillforwardanyerrorthatwereceivefromthedatabaseasitis.
Finally,weonlyhavetoreturnthefsobjectandclosethefactoryfunctionusingthefollowinglinesofcode:
returnfs;
}
Ournewadapterisnowready;ifwenowwriteasmalltestmodule,wecantrytouseit:
varfs=require('fs');
fs.writeFile('file.txt','Hello!',function(){
fs.readFile('file.txt',{encoding:'utf8'},function(err,
res){
console.log(res);
});
});
//trytoreadamissingfile
fs.readFile('missing.txt',{encoding:'utf8'},function(err,
res){
console.log(err);
});
TheprecedingcodeusestheoriginalfsAPItoperformafewreadandwriteoperationsonthefilesystemandshouldprintsomethinglikethefollowingtotheconsole:
{[Error:ENOENT,open'missing.txt']errno:34,code:
'ENOENT',path:'missing.txt'}
Hello!
Now,wecantrytoreplacethefsmodulewithouradapter,asfollows:
varlevelup=require('level');
varfsAdapter=require('./fsAdapter');
vardb=levelup('./fsDB',{valueEncoding:'binary'});
varfs=fsAdapter(db);
Runningagainourprogramshouldproducethesameoutput,exceptthefactthatnoneofthefilethatwespecifiedisreadorwrittenusingthefilesystem;instead,anyoperationperformedusingouradapterwillbeconvertedintoanoperationperformedonaLevelUPdatabase.
Theadapterthatwejustcreatedmightlooksilly;what'sthepurposeofusingadatabaseinplaceoftherealfilesystem?However,weshouldrememberthatLevelUPitselfhasadaptersthatenablethedatabasetoalsoruninthebrowser;oneoftheseadaptersislevel.js(https://npmjs.org/package/level-js).Now,ouradaptershouldmakeperfectsense;wecanthinkofusingittosharewiththebrowsercode,whichreliesonthefsmodule!Forexample,thewebspiderthatwecreatedinChapter2,AsynchronousControlFlowPatterns,usesthefsAPItostorethewebpagesdownloadedduringitsoperations;ouradapterwillallowittoruninthebrowser,byapplyingonlyminormodifications!WesoonrealizethatAdapterisanextremelyimportantpatternalsowhenitcomestosharingcodewiththebrowser,aswewillseeinmoredetailinChapter6,Recipes.
InthewildThereareplentyofreal-worldexamplesoftheAdapterpattern:welistsomeofthemostnotableexampleshereforyoutoexploreandanalyze:
WealreadyknowthatLevelUPisabletorunwithdifferentstoragebackends,fromthedefaultLevelDBtoIndexedDBinthebrowser.Thisismadepossiblebythevariousadaptersthatarecreatedtoreplicatetheinternal(private)LevelUPAPI.Takealookatsomeofthemtoseehowtheyareimplemented:https://github.com/rvagg/node-levelup/wiki/Modules#storage-back-ends.jugglingdbisamulti-databaseORMandofcourse,multipleadaptersareusedtomakeitcompatiblewithdifferentdatabases.Takealookatsomeofthemathttps://github.com/1602/jugglingdb/tree/master/lib/adapters.Theperfectcomplementtotheexamplethatwecreatedislevel-filesystem(https://www.npmjs.org/package/level-filesystem),whichistheproperimplementationofthefsAPIontopofLevelUP.
StrategyTheStrategypatternenablesanobject,calledtheContext,tosupportvariationsinitslogicbyextractingthevariablepartsintoseparate,interchangeableobjectscalledStrategies.Thecontextimplementsthecommonlogicofafamilyofalgorithms,whileastrategyimplementsthemutableparts,allowingthecontexttoadaptitsbehaviordependingondifferentfactorssuchasaninputvalue,asystemconfiguration,oruserpreferences.Thestrategiesareusuallypartofafamilyofsolutionsandallofthemimplementthesameinterface,whichistheonethatisexpectedbythecontext.Thefollowingfigureshowsthesituationwejustdescribed:
Theprecedingfigureshowshowthecontextobjectcanplugdifferentstrategiesintoitsstructure,astheywerereplaceablepartsofapieceofmachinery.Imagineacar,itstirescanbeconsidereditsstrategytoadapttothedifferentroadconditions.Wecanfitthewintertirestogoonsnowyroadsthankstotheirstuds,whilewecandecidetofithigh-performancetirestogomainlyonmotorwaysforalongtrip.Ontheonehand,wedon'twanttochangetheentirecarforthistobepossible,andontheother,wedon'twantacarwitheightwheelssothatitcangooneverypossibleroad.
Wequicklyunderstandhowpowerfulthispatternis;notonlyithelpswithseparatingtheconcernswithinanalgorithmbutitalsoenablesittohaveabetterflexibilityandadapttodifferentvariationsofthesameproblem.
TheStrategypatternisparticularlyusefulinallthosesituationswheresupportingvariationsofanalgorithmrequirescomplexconditionallogic(lotsofif-elseorswitchstatements)ormixingtogetherdifferentalgorithmsofthesamefamily.ImagineanobjectcalledOrderthatrepresentsanonlineorderofane-commercewebsite.Theobjecthasamethodcalledpay()that,asitsays,finalizestheorderandtransfersthefundsfromtheusertotheonlinestore.
Tosupportdifferentpaymentsystems,wehaveacoupleofoptionsasfollows:
Useanif/elsestatementinthepay()methodtocompletetheoperationbasedonthechosenpaymentoptionDelegatethelogicofthepaymenttoastrategyobjectthatimplementsthelogicforthespecificpaymentgatewayselectedbytheuser
Inthefirstsolution,ourOrderobjectcannotsupportotherpaymentmethodsunlessitscodeismodified.Also,thiscanbecomequitecomplexwhenthenumberofpaymentoptionsgrows.Instead,usingtheStrategypatternenablestheOrderobjecttosupportavirtuallyunlimitednumberofpaymentmethodsandkeepsitsscopelimitedtoonlymanagingthedetailsoftheuser,thepurchaseditems,andrelativeprice,whiledelegatingthejobofcompletingthepaymenttoanotherobject.
Let'snowdemonstratethispatternwithasimple,realisticexample.
Multi-formatconfigurationobjectsLet'sconsideranobjectcalledConfigthatholdsasetofconfigurationparametersusedbyanapplication,suchasthedatabaseURL,thelisteningportoftheserver,andsoon.TheConfigobjectshouldbeabletoprovideasimpleinterfacetoaccesstheseparametersbutalsoawaytoimportandexporttheconfigurationusingapersistentstorage,suchasafile.Wewanttobeabletosupportdifferentformatstostoretheconfiguration,asforexample,JSON,INI,orYAML.
ByapplyingwhatwelearnedabouttheStrategypattern,wecanimmediatelyidentifythevariablepartoftheconfigobject,whichisthefunctionalitythatallowsustoserializeanddeserializetheconfiguration.Thisisgoingtobeourstrategy.
Let'screateanewmodulecalledconfig.jsandlet'sdefinethegenericpartofourconfigurationmanager:
varfs=require('fs');
varobjectPath=require('object-path');
functionConfig(strategy){
this.data={};
this.strategy=strategy;
}
Config.prototype.get=function(path){
returnobjectPath.get(this.data,path);
}
Config.prototype.set=function(path,value){
returnobjectPath.set(this.data,path,value);
}
Intheprecedingcode,weencapsulatetheconfigurationdataintoaninstancevariableandthenweprovidetheset()andget()methodsthatallowustoaccesstheconfigurationpropertiesusingadottedpathnotation(forexample,property.subProperty)byleveraginganpmlibrarycalledobject-path(https://npmjs.org/package/object-path).Intheconstructor,wealsotakeinastrategyasinput,whichrepresentsanalgorithmforparsingandserializingthedata.
Let'snowseehowwearegoingtousestrategy,bywritingtheremainingpartoftheConfigclass:
Config.prototype.read=function(file){
console.log('Deserializingfrom'+file);
this.data=this.strategy.deserialize(fs.readFileSync(file,
'utf-8'));
}
Config.prototype.save=function(file){
console.log('Serializingto'+file);
fs.writeFileSync(file,this.strategy.serialize(this.data));
}
module.exports=Config;
Inthepreviouscode,whenreadingtheconfigurationfromafile,wedelegatethedeserializationtasktothestrategy;then,whenwewanttosavetheconfigurationintoafile,weusestrategytoserializetheconfiguration.ThissimpledesignallowstheConfigobjecttosupportdifferentfileformatswhenloadingandsavingitsdata.
Todemonstratethis,let'screateacoupleofstrategiesintoafilecalledstrategies.js.Let'sstartwithastrategyforparsingandserializingJSONdata:
module.exports.json={
deserialize:function(data){
returnJSON.parse(data);
},
serialize:function(data){
returnJSON.stringify(data,null,'');
}
}
Nothingreallycomplicated!Ourstrategysimplyimplementstheagreedinterface,sothatitcanbeusedbytheConfigobject.
Similarly,thenextstrategywearegoingtocreateallowsustosupporttheINIfileformat:
varini=require('ini');//->https://npmjs.org/package/ini
module.exports.ini={
deserialize:function(data){
returnini.parse(data);
},
serialize:function(data){
returnini.stringify(data);
}
}
Now,toshowyouhoweverythingcomestogether,let'screateafilenamedconfigTest.jsandlet'strytoloadandsaveasampleconfigurationusingdifferentformats:
varConfig=require('./config');
varstrategies=require('./strategies');
varjsonConfig=newConfig(strategies.json);
jsonConfig.read('samples/conf.json');
jsonConfig.set('book.nodejs','designpatterns');
jsonConfig.save('samples/conf_mod.json');
variniConfig=newConfig(strategies.ini);
iniConfig.read('samples/conf.ini');
iniConfig.set('book.nodejs','designpatterns');
iniConfig.save('samples/conf_mod.ini');
OurtestmodulerevealsthepropertiesoftheStrategypattern.WedefinedonlyoneConfigclass,whichimplementsthecommonpartsofourconfigurationmanager,whilechangingthestrategyusedforserializinganddeserializingallowedustocreatedifferentConfiginstancessupportingdifferentfileformats.
Theprecedingexampleshowsonlyoneofthepossiblealternativesthatwehadforselectingthestrategy.Othervalidapproachesmighthavebeenthefollowing:
Creatingtwodifferentstrategyfamilies:oneforthedeserializationandtheotherfortheserialization.Thiswouldhaveallowedreadingfromaformatandsavingintoanother.Dynamicallyselectingthestrategydependingontheextensionofthefileprovided;theConfigobjectcouldhavemaintainedamapextension->strategyandusedittoselecttherightalgorithmforthegivenextension.
Aswecansee,thereareseveraloptionsforselectingthestrategytouseandtherightoneonlydependsonourrequirementsandthetrade-offintermsoffeatures/simplicitywewanttoobtain.
Also,theimplementationofthepatternitselfcanvaryalot,forexample,inits
simplestform,thecontextandthestrategycanbothbesimplefunctions:
functioncontext(strategy){...}
Eventhoughtheprecedingsituationmightseeminsignificant,itshouldnotbeunderestimatedinaprogramminglanguage,suchasJavaScript,wherefunctionsarefirst-classcitizensandusedasmuchasfull-fledgedobjects.
Betweenallthesevariationsthough,whatdoesnotchangeistheideabehindthepattern,asalwaystheimplementationcanslightlychangebutthecoreconceptsthatdrivethepatternarealwaysthesame.
InthewildPassport.js(http://passportjs.org)isanauthenticationframeworkforNode.jswhichallowstoaddsupportfordifferentauthenticationschemesintoawebserver.WithPassport,wecanprovideaLoginwithFacebookorLoginwithTwitterfunctionalitytoourwebapplicationwithminimaleffort.PassportusestheStrategypatterntoseparatethecommonlogicrequiredduringanauthenticationprocessfromthepartsthatcanchange,namelytheactualauthenticationstep.Forexample,wemightwanttouseOAuthinordertoobtainanaccessTokentoaccessaFacebookorTwitterprofile,orsimplyusealocaldatabasetoverifyausername/passwordpair.ForPassport,thesearealldifferentstrategiesforcompletingtheauthenticationprocess,andaswecanimagine,thisallowsthelibrarytosupportavirtuallyunlimitednumberofauthenticationservices.Takealookatthenumberofdifferentauthenticationproviderssupportedathttp://passportjs.org/guide/providers,togetanideaofwhattheStrategypatterncando.
StateStateisavariationoftheStrategypatternwherethestrategychangesdependingonthestateofthecontext.Wehaveseenintheprevioussectionhowastrategycanbeselectedbasedondifferentvariablessuchasuserpreferences,aconfigurationparameter,theinputprovidedandoncethisselectionisdone,thestrategystaysunchangedfortherestofthelifespanofthecontext.
IntheStatepatterninstead,thestrategy(alsocalledStateinthiscircumstance)isdynamicandcanchangeduringthelifetimeofthecontext,thusallowingitsbehaviortoadaptdependingonitsinternalstate,asshowninthefollowingfigure:
ImaginethatwehaveahotelbookingsystemandanobjectcalledReservationthatmodelsaroomreservation.Thisisaclassicalsituationwherewehavetoadaptthebehaviorofanobjectbasedonitsstate.Considerthefollowingseriesofevents:
1. Whenthereservationisinitiallycreated,theusercanconfirm(usingconfirm())thereservation;ofcourse,theycannotcancel(usingcancel())it,becauseit'sstillnotconfirmed.Theycanhoweverdelete(usingdelete())itiftheychangetheirmindbeforebuying.
2. Oncethereservationisconfirmed,usingtheconfirm()functionagaindoesnotmakeanysense;however,nowitshouldbepossibletocancelthereservationbutnottodeleteitanylonger,becauseithastobekeptfortherecord.
3. Onthedaybeforethereservationdate,itshouldnotbepossibletocancelthereservation;it'stoolateforthat.
Now,imaginethatwehavetoimplementthereservationsystemthatwedescribedinonemonolithicobject;wecanalreadypicturealltheif-elseorswitchstatements
thatwewouldhavetowritetoenable/disableeachactiondependingonthestateofthereservation.
TheStatepatterninsteadisperfectinthissituation:therewillbethreestrategiesandallimplementingthethreemethodsdescribed(confirm(),cancel(),delete())andeachoneimplementingonlyonebehavior,theonecorrespondingtothemodeledstate.Byusingthispattern,itshouldbeveryeasyfortheReservationobjecttoswitchfromonebehaviortoanother;thiswillsimplyrequiretheactivationofadifferentstrategyoneachstatechange.
Thestatetransitioncanbeinitiatedandcontrolledbythecontextobject,bytheclientcode,orbytheStateobjectsthemselves.Thislastoptionusuallyprovidesthebestresultsintermsofflexibilityanddecouplingasthecontextdoesnothavetoknowaboutallthepossiblestatesandhowtotransitionbetweenthem.
Implementingabasicfail-safesocketLet'snowworkonaconcreteexamplesothatwecanapplywhatwelearnedabouttheStatepattern.Let'sbuildaclientTCPsocketthatdoesnotfailwhentheconnectionwiththeserverislost;instead,wewanttoqueueallthedatasentduringthetimeinwhichtheserverisofflineandthentrytosenditagainassoonastheconnectionisre-established.Wewanttoleveragethissocketinthecontextofasimplemonitoringsystem,whereasetofmachinessendsomestatisticsabouttheirresourceutilizationatregularintervals;iftheserverthatcollectstheseresourcesgoesdown,oursocketwillcontinuetoqueuethedatalocallyuntiltheservercomesbackonline.
Let'sstartbycreatinganewmodulecalledfailsafeSocket.jsthatrepresentsourcontextobject:
varOfflineState=require('./offlineState');
varOnlineState=require('./onlineState');
functionFailsafeSocket(options){//[1]
this.options=options;
this.queue=[];
this.currentState=null;
this.socket=null;
this.states={
offline:newOfflineState(this),
online:newOnlineState(this)
}
this.changeState('offline');
}
FailsafeSocket.prototype.changeState=function(state){//[2]
console.log('Activatingstate:'+state);
this.currentState=this.states[state];
this.currentState.activate();
}
FailsafeSocket.prototype.send=function(data){//[3]
this.currentState.send(data);
}
module.exports=function(options){
returnnewFailsafeSocket(options);
};
TheFailsafeSocketpseudoclassismadeofthreemainelements:
1. Theconstructorinitializesvariousdatastructures,includingthequeuethatwillcontainanydatasentwhilethesocketisoffline.Also,itcreatesasetoftwostates,oneforimplementingthebehaviorofthesocketwhileit'sofflineandanotheronewhenthesocketisonline.
2. ThechangeState()methodisresponsiblefortransitioningfromonestatetoanother.ItsimplyupdatesthecurrentStateinstancevariableandcallsactivate()onthetargetstate.
3. Thesend()methodisthefunctionalityofthesocket,thisiswherewewanttohaveadifferentbehaviorbasedontheoffline/onlinestate.Aswecansee,thisisdonebydelegatingtheoperationtothecurrentlyactivestate.
Let'snowseehowthetwostateslooklike,startingfromtheofflineState.jsmodule:
varjot=require('json-over-tcp');//[1]
functionOfflineState(failsafeSocket){
this.failsafeSocket=failsafeSocket;
}
module.exports=OfflineState;
OfflineState.prototype.send=function(data){//[2]
this.failsafeSocket.queue.push(data);
}
OfflineState.prototype.activate=function(){//[3]
varself=this;
functionretry(){
setTimeout(function(){
self.activate();
},500);
}
self.failsafeSocket.socket=jot.connect(
self.failsafeSocket.options,
function(){
self.failsafeSocket.socket.removeListener('error',
retry);
self.failsafeSocket.changeState('online');
}
);
self.failsafeSocket.socket.once('error',retry);
}
Themodulethatwecreatedisresponsibleformanagingthebehaviorofthesocketwhileit'soffline;thisishowitworks:
1. InsteadofusingarawTCPsocket,wewillusealittlelibrarycalledjson-over-tcp(https://npmjs.org/package/json-over-tcp),whichwillallowustoeasilysendJSONobjectsoveraTCPconnection.
2. Thesend()methodisonlyresponsibleforqueuinganydataitreceives;weareassumingthatweareoffline,sothat'sallweneedtodo.
3. Theactivate()methodtriestoestablishaconnectionwiththeserverusingjson-over-tcp.Iftheoperationfails,ittriesagainafter500milliseconds.Itcontinuestryinguntilavalidconnectionisestablished,inwhichcasethestateoffailsafeSocketistransitionedtoonline.
Next,let'simplementtheonlineState.jsmodule,andthen,let'simplementtheonlineStatestrategyasfollows:
functionOnlineState(failsafeSocket){
this.failsafeSocket=failsafeSocket;
}
module.exports=OnlineState;
OnlineState.prototype.send=function(data){//[1]
this.failsafeSocket.socket.write(data);
};
OnlineState.prototype.activate=function(){//[2]
varself=this;
self.failsafeSocket.queue.forEach(function(data){
self.failsafeSocket.socket.write(data);
});
self.failsafeSocket.queue=[];
self.failsafeSocket.socket.once('error',function(){
self.failsafeSocket.changeState('offline');
});
}
TheOnlineStatestrategyisverysimpleandisexplainedasfollows:
1. Thesend()methodwritesthedatadirectlyintothesocket,asweassumewe
areonline.2. Theactivate()methodflushesanydatathatwasqueuedwhilethesocketwas
offlineanditalsostartslisteningforanyerrorevent;wewilltakethisasasymptomthatthesocketwentoffline(forsimplicity).Whenthishappens,wetransitiontotheofflinestate.
That'sitforourfailsafeSocket;nowwearereadytobuildasampleclientandaservertotryitout.Let'sputtheservercodeinamodulenamedserver.js:
varjot=require('json-over-tcp');
varserver=jot.createServer(5000);
server.on('connection',function(socket){
socket.on('data',function(data){
console.log('Clientdata',data);
});
});
server.listen(5000,function(){console.log('Started')});
Thentheclientsidecode,whichiswhatwearereallyinterestedin,goesintoclient.js:
varcreateFailsafeSocket=require('./failsafeSocket');
varfailsafeSocket=createFailsafeSocket({port:5000});
setInterval(function(){
//sendcurrentmemoryusage
failsafeSocket.send(process.memoryUsage());
},1000);
OurserversimplyprintsanyJSONmessageitreceivestotheconsole,whileourclientsaresendingameasurementoftheirmemoryutilizationeverysecond,leveragingaFailsafeSocketobject.
Totrythesmallsystemthatwebuilt,weshouldrunboththeclientandtheserver,thenwecantestthefeaturesoffailsafeSocketbystoppingandthenrestartingtheserver.Weshouldseethatthestateoftheclientchangesbetweenonlineandoffline,andthatanymemorymeasurementcollectedwhiletheserverisofflineisqueuedandthenresentassoonastheservergoesbackonline.
ThissampleshouldbeacleardemonstrationofhowtheStatepatterncanhelpincreasethemodularityandreadabilityofacomponentthathastoadaptitsbehaviordependingonitsstate.
Note
TheFailsafeSocketclassthatwebuiltinthissectionisonlyfordemonstratingtheStatepatternanddoesn'twanttobeacompleteand100percent-reliablesolutiontohandleconnectivityissueswithinTCPsockets.Forexample,wearenotverifyingthatallthedatawrittenintothesocketstreamisreceivedbytheserver,whichwouldrequiresomemorecodenotstrictlyrelatedtothepatternthatwewantedtodescribe.
TemplateThenextpatternthatwearegoingtoanalyzeiscalledTemplateanditalsohasalotincommonwiththeStrategypattern.Templateconsistsofdefininganabstractpseudoclassthatrepresentstheskeletonofanalgorithmwheresomeofitsstepsareleftundefined.Subclassescanthenfillthegapsinthealgorithmbyimplementingthemissingsteps,calledtemplatemethods.Theintentofthispatternismakingitpossibletodefineafamilyofclassesthatareallvariationsofasimilaralgorithm.ThefollowingUMLdiagramshowsthestructurethatwejustdescribed:
Thethreeconcreteclassesshowninthepreviousdiagram,extendTemplateandprovideanimplementationfortemplateMethod(),whichisabstractorpurevirtual,tousetheC++terminology;inJavaScriptthismeansthatthemethodisleftundefinedorisassignedtoafunctionthatalwaysthrowsanexception,indicatingthefactthatthemethodhastobeimplemented.TheTemplatepatterncanbeconsideredmoreclassicallyobject-orientedthantheotherpatternswehaveseensofar,becauseinheritanceisacorepartofitsimplementation.
ThepurposeofTemplateandStrategyisverysimilar,butthemaindifferencebetweenthetwoliesintheirstructureandimplementation.Bothallowustochangesomepartsofanalgorithmwhilereusingthecommonparts;however,whileStrategyallowsustodoitdynamicallyandpossiblyatruntime,withTemplate,thecompletealgorithmisdeterminedthemomenttheconcreteclassisdefined.Undertheseassumptions,theTemplatepatternmightbemoresuitableinthosecircumstanceswherewewanttocreateprepackagedvariationsofanalgorithm.Asalways,thechoicebetweenonepatternandtheotherisuptothedeveloperwhohastoconsiderthevariousprosandconsforeachusecase.
Aconfigurationmanagertemplate
TohaveabetterideaofthedifferencesbetweenStrategyandTemplate,let'snowre-implementtheConfigobjectthatwedefinedinthesectionabouttheStrategypattern,butthistimeusingTemplate.LikeinthepreviousversionoftheConfigobject,wewanttohavetheabilitytoloadandsaveasetofconfigurationpropertiesusingdifferentfileformats.
Let'sstartbydefiningthetemplateclass;wewillcallitConfigTemplate:
varfs=require('fs');
varobjectPath=require('object-path');
functionConfigTemplate(){}
ConfigTemplate.prototype.read=function(file){
console.log('Deserializingfrom'+file);
this.data=this._deserialize(fs.readFileSync(file,'utf-
8'));
}
ConfigTemplate.prototype.save=function(file){
console.log('Serializingto'+file);
fs.writeFileSync(file,this._serialize(this.data));
}
ConfigTemplate.prototype.get=function(path){
returnobjectPath.get(this.data,path);
}
ConfigTemplate.prototype.set=function(path,value){
returnobjectPath.set(this.data,path,value);
}
ConfigTemplate.prototype._serialize=function(){
thrownewError('_serialize()mustbeimplemented');
}
ConfigTemplate.prototype._deserialize=function(){
thrownewError('_deserialize()mustbeimplemented');
}
module.exports=ConfigTemplate;
ThenewConfigTemplateclassdefinestwotemplatemethods:_deserialize()and_serialize(),thatareneededtocarryouttheloadingandsavingoftheconfiguration.Theunderscoreatthebeginningoftheirnamesindicatesthattheyareforinternaluseonly,aneasywaytoflagprotectedmethods.Since,inJavaScript,wecannotdeclareamethodasabstract,wesimplydefinethemasstubs,throwinganexceptioniftheyareinvoked(inotherwords,iftheyarenotoverriddenbyaconcretesubclass).
Let'snowcreateaconcreteclassusingourtemplate,forexample,onethatallowsustoloadandsavetheconfigurationusingtheJSONformat:
varutil=require('util');
varConfigTemplate=require('./configTemplate');
functionJsonConfig(){}
util.inherits(JsonConfig,ConfigTemplate);
JsonConfig.prototype._deserialize=function(data){
returnJSON.parse(data);
};
JsonConfig.prototype._serialize=function(data){
returnJSON.stringify(data,null,'');
}
module.exports=JsonConfig;
TheJsonConfigclassinheritsfromourtemplate,theConfigTemplateclass,andprovidesaconcreteimplementationforthe_deserialize()and_serialize()methods.
TheJsonConfigclasscannowbeusedasastandaloneconfigurationobject,withouttheneedtospecifyastrategyforserializationanddeserialization,asitisbakedintheclassitself:
varJsonConfig=require('./jsonConfig');
varjsonConfig=newJsonConfig();
jsonConfig.read('samples/conf.json');
jsonConfig.set('nodejs','designpatterns');
jsonConfig.save('samples/conf_mod.json');
Withminimaleffort,theTemplatepatternallowedustoobtainanew,fullyworkingconfigurationmanagerbyreusingthelogicandtheinterfaceinheritedfromtheparenttemplateclassandprovidingonlytheimplementationofafewabstractmethods.
InthewildThispatternshouldnotsoundentirelynewtous.WealreadyencountereditinChapter3,CodingwithStreams,whenwewereextendingthedifferentstreamclassestoimplementourcustomstreams.Inthatcontext,thetemplatemethodswerethe_write(),_read(),_transform(),or_flush()methods,dependingonthestreamclassthatwewantedtoimplement.Tocreateanewcustomstream,weneededtoinheritfromaspecificabstractstreamclass,providinganimplementation
forthetemplatemethods.
MiddlewareOneofthemostdistinctivepatternsinNode.jsisdefinitelymiddleware.Unfortunatelyit'salsooneofthemostconfusingfortheinexperienced,especiallyfordeveloperscomingfromtheenterpriseprogrammingworld.Thereasonforthedisorientationisprobablyconnectedwiththemeaningofthetermmiddleware,whichintheenterprisearchitecture'sjargonrepresentsthevarioussoftwaresuitesthathelptoabstractlowerlevelmechanismssuchasOSAPIs,networkcommunications,memorymanagement,andsoon,allowingthedevelopertofocusonlyonthebusinesscaseoftheapplication.Inthiscontext,thetermmiddlewarerecallstopicssuchasCORBA,EnterpriseServiceBus,Spring,JBoss,butinitsmoregenericmeaningitcanalsodefineanykindofsoftwarelayerthatactslikeagluebetweenlowerlevelservicesandtheapplication(literallythesoftwareinthemiddle).
MiddlewareinExpressExpress(http://expressjs.com)popularizedthetermmiddlewareintheNode.jsworld,bindingittoaveryspecificdesignpattern.Inexpress,infact,amiddlewarerepresentsasetofservices,typicallyfunctions,thatareorganizedinapipelineandareresponsibleforprocessingincomingHTTPrequestsandrelativeresponses.Anexpressmiddlewarehasthefollowingsignature:
function(req,res,next){...}
WherereqistheincomingHTTPrequest,resistheresponse,andnextisthecallbacktobeinvokedwhenthecurrentmiddlewarehascompleteditstasksandthatinturntriggersthenextmiddlewareinthepipeline.
Examplesofthetaskscarriedoutbyanexpressmiddlewareareasthefollowing:
ParsingthebodyoftherequestCompressing/decompressingrequestsandresponsesProducingaccesslogsManagingsessionsProvidingCross-siteRequestForgery(CSRF)protection
Ifwethinkaboutit,thesearealltasksthatarenotstrictlyrelatedtothemainfunctionalityofanapplication,rather,theyareaccessories,componentsprovidingsupporttotherestoftheapplicationandallowingtheactualrequesthandlerstofocusonlyontheirmainbusinesslogic.Essentially,thosetasksaresoftwareinthemiddle.
MiddlewareasapatternThetechniqueusedtoimplementmiddlewareinexpressisnotnew;infact,itcanbeconsideredtheNode.jsincarnationoftheInterceptingFilterpatternandtheChainofResponsibilitypattern.Inmoregenericterms,italsorepresentsaprocessingpipeline,whichremindsusaboutstreams.Today,inNode.js,thewordmiddlewareisusedwellbeyondtheboundariesoftheexpressframework,andindicatesaparticularpatternwherebyasetofprocessingunits,filters,andhandlers,undertheformoffunctionsareconnectedtoformanasynchronoussequenceinordertoperformpreprocessingandpostprocessingofanykindofdata.Themainadvantageofthispatternisflexibility;infact,thispatternallowsustoobtainaplugininfrastructurewithincrediblylittleeffort,providinganunobtrusivewayforextendingasystemwithnewfiltersandhandlers.
TipIfyouwanttoknowmoreabouttheInterceptingFilterpattern,thefollowingarticleisagoodstartingpoint:http://www.oracle.com/technetwork/java/interceptingfilter-142169.html.AniceoverviewoftheChainofResponsibilitypatternisavailableatthisURLhttp://java.dzone.com/articles/design-patterns-uncovered-chain-of-responsibility.
Thefollowingdiagramshowsthecomponentsofthemiddlewarepattern:
TheessentialcomponentofthepatternistheMiddlewareManager,whichisresponsiblefororganizingandexecutingthemiddlewarefunctions.Themostimportantimplementationdetailsofthepatternareasfollows:
Newmiddlewarecanberegisteredbyinvokingtheuse()function(thenameofthisfunctionisacommonconventioninmanyimplementationsofthispattern,butwecanchooseanyname).Usually,newmiddlewarecanonlybeappendedat
theendofthepipeline,butthisisnotastrictrule.Whennewdatatoprocessisreceived,theregisteredmiddlewareisinvokedinanasynchronoussequentialexecutionflow.Eachunitinthepipelinereceivesininputtheresultoftheexecutionofthepreviousunit.Eachmiddlewarecandecidetostopfurtherprocessingofthedatabysimplynotinvokingitscallbackorbypassinganerrortothecallback.Anerrorsituationusuallytriggerstheexecutionofanothersequenceofmiddlewarethatisspecificallydedicatedtohandlingerrors.
Thereisnostrictruleonhowthedataisprocessedandpropagatedinthepipeline.Thestrategiesinclude:
AugmentingthedatawithadditionalpropertiesorfunctionsReplacingthedatawiththeresultofsomekindofprocessingMaintainingtheimmutabilityofthedataandalwaysreturningfreshcopiesasresultoftheprocessing
TherightapproachthatweneedtotakedependsonthewaytheMiddlewareManagerisimplementedandonthetypeofprocessingcarriedoutbythemiddlewareitself.
Creatingamiddlewareframeworkfor ØMQLet'snowdemonstratethepatternbybuildingamiddlewareframeworkaroundtheØMQ(http://zeromq.org)messaginglibrary.ØMQ(alsoknownasZMQ,orZeroMQ)providesasimpleinterfaceforexchangingatomicmessagesacrossthenetworkusingavarietyofprotocols;itshinesforitsperformances,anditsbasicsetofabstractionsarespecificallybuilttofacilitatetheimplementationofcustommessagingarchitectures.Forthisreason,ØMQisoftenchosentobuildcomplexdistributedsystems.
NoteInChapter8,MessagingandIntegrationPatterns,wewillhavethechancetoanalyzethefeaturesofØMQinmoredetail.
TheinterfaceofØMQisprettylow-level,itonlyallowsustousestringsandbinarybuffersformessages,soanyencodingorcustomformattingofdatahastobeimplementedbytheusersofthelibrary.
Inthenextexample,wearegoingtobuildamiddlewareinfrastructuretoabstractthe
preprocessingandpostprocessingofthedatapassingthroughaØMQsocket,sothatwecantransparentlyworkwithJSONobjectsbutalsoseamlesslycompressthemessagestravelingoverthewire.
NoteBeforecontinuingwiththeexample,pleasemakesuretoinstalltheØMQnativelibrariesfollowingtheinstructionsatthisURL:http://zeromq.org/intro:get-the-software.Anyversioninthe4.0branchshouldbeenoughforworkingonthisexample.
TheMiddlewareManagerThefirststeptobuildamiddlewareinfrastructurearoundØMQistocreateacomponentthatisresponsibleforexecutingthemiddlewarepipelinewhenanewmessageisreceivedorsent.Forthepurpose,let'screateanewmodulecalledzmqMiddlewareManager.jsandlet'sstartdefiningit:
functionZmqMiddlewareManager(socket){
this.socket=socket;
this.inboundMiddleware=[];//[1]
this.outboundMiddleware=[];
varself=this;
socket.on('message',function(message){//[2]
self.executeMiddleware(self.inboundMiddleware,{
data:message
});
});
}
module.exports=ZmqMiddlewareManager;
Thisfirstcodefragmentdefinesanewconstructorforournewcomponent.ItacceptsaØMQsocketasanargumentand:
1. Createstwoemptyliststhatwillcontainourmiddlewarefunctions,onefortheinboundmessagesandanotheronefortheoutboundmessages.
2. Immediately,itstartslisteningforthenewmessagescomingfromthesocketbyattachinganewlistenertothe'message'event.Inthelistener,weprocesstheinboundmessagebyexecutingtheinboundMiddlewarepipeline.
ThenextmethodoftheZmqMiddlewareManagerprototypeisresponsibleforexecutingthemiddlewarewhenanewmessageissentthroughthesocket:
ZmqMiddlewareManager.prototype.send=function(data){
varself=this;
varmessage={
data:data
};
self.executeMiddleware(self.outboundMiddleware,message,
function(){
self.socket.send(message.data);
}
);
}
ThistimethemessageisprocessedusingthefiltersintheoutboundMiddlewarelistandthenpassedtosocket.send()fortheactualnetworktransmission.
Now,weneedasmallmethodtoappendnewmiddlewarefunctionstoourpipelines;wealreadymentionedthatsuchamethodisconventionallycalleduse():
ZmqMiddlewareManager.prototype.use=function(middleware){
if(middleware.inbound){
this.inboundMiddleware.push(middleware.inbound);
}
if(middleware.outbound){
this.outboundMiddleware.unshift(middleware.outbound);
}
}
Eachmiddlewarecomesinpairs;inourimplementationit'sanobjectthatcontainstwoproperties,inboundandoutbound,thatcontainthemiddlewarefunctionstobeaddedtotherespectivelist.
It'simportanttoobserveherethattheinboundmiddlewareispushedtotheendoftheinboundMiddlewarelist,whiletheoutboundmiddlewareisinsertedatthebeginningoftheoutboundMiddlewarelist.Thisisbecausecomplementaryinbound/outboundmiddlewarefunctionsusuallyneedtobeexecutedinaninvertedorder.Forexample,ifwewanttodecompressandthendeserializeaninboundmessageusingJSON,itmeansthatfortheoutbound,weshouldinsteadfirstserializeandthencompress.
NoteIt'simportanttounderstandthatthisconventionfororganizingthemiddlewareinpairsisnotstrictlypartofthegeneralpattern,butonlyanimplementationdetailofourspecificexample.
Now,it'stimetodefinethecoreofourcomponent,thefunctionthatisresponsibleforexecutingthemiddleware:
ZmqMiddlewareManager.prototype.executeMiddleware=
function(middleware,arg,finish){
varself=this;
(functioniterator(index){
if(index===middleware.length){
returnfinish&&finish();
}
middleware[index].call(self,arg,function(err){
if(err){
console.log('Therewasanerror:'+err.message);
}
iterator(++index);
});
})(0);
}
Theprecedingcodeshouldlookveryfamiliar;infact,itisasimpleimplementationoftheasynchronoussequentialiterationpatternthatwelearnedinChapter2,AsynchronousControlFlowPatterns.Eachfunctioninthemiddlewarearrayreceivedininputisexecutedoneaftertheother,andthesameargobjectisprovidedasanargumenttoeachmiddlewarefunction;thisisthetrickthatmakesitpossibletopropagatethedatafromonemiddlewaretothenext.Attheendoftheiteration,thefinish()callbackisinvoked.
NotePleasenotethatforbrevitywearenotsupportinganerrormiddlewarepipeline.Normally,whenamiddlewarefunctionpropagatesanerror,anothersetofmiddlewarespecificallydedicatedtohandlingerrorsisexecuted.Thiscanbeeasilyimplementedusingthesametechniquethatwearedemonstratinghere.
AmiddlewaretosupportJSONmessagesNowthatwehaveimplementedourMiddlewareManager,wecancreateapairofmiddlewarefunctionstodemonstratehowtoprocessinboundandoutboundmessages.Aswesaid,oneofthegoalsofourmiddlewareinfrastructureishavingafilterthatserializesanddeserializesJSONmessages,solet'screateanewmiddlewaretotakecareofthis.Inanewmodulecalled'middleware.js'let'sincludethefollowingcode:
module.exports.json=function(){
return{
inbound:function(message,next){
message.data=JSON.parse(message.data.toString());
next();
},
outbound:
function(message,next){
message.data=newBuffer(JSON.stringify(message.data));
next();
}
}
}
Thejsonmiddlewarethatwejustcreatedisverysimple:
Theinboundmiddlewaredeserializesthemessagereceivedasaninputandassignstheresultbacktothedatapropertyofmessage,sothatitcanbefurtherprocessedalongthepipelineTheoutboundmiddlewareserializesanydatafoundintomessage.data
Pleasenotehowthemiddlewaresupportedbyourframeworkisquitedifferentfromtheoneusedinexpress;thisistotallynormalandaperfectdemonstrationofhowwecanadaptthispatterntofitourspecificneed.
UsingtheØMQmiddlewareframeworkWearenowreadytousethemiddlewareinfrastructurethatwejustcreated.Todothat,wearegoingtobuildaverysimpleapplication,withaclientsendingapingtoaserveratregularintervalsandtheserverechoingbackthemessagereceived.
Fromanimplementationperspective,wearegoingtorelyonarequest/replymessagingpatternusingthereq/repsocketpairprovidedbyØMQ(http://zguide.zeromq.org/page:all#Ask-and-Ye-Shall-Receive).WewillthenwrapthesocketswithourzmqMiddlewareManagertogetalltheadvantagesfromthemiddlewareinfrastructurethatwebuilt,includingthemiddlewareforserializing/deserializingJSONmessages.
Theserver
Let'sstartbycreatingtheserverside(server.js).Inthefirstpartofthemoduleweinitializeourcomponents:
varzmq=require('zmq');
varZmqMiddlewareManager=require('./zmqMiddlewareManager');
varmiddleware=require('./middleware');
varreply=zmq.socket('rep');
reply.bind('tcp://127.0.0.1:5000');
Intheprecedingcode,weloadedtherequireddependenciesandbindaØMQ'rep'(reply)sockettoalocalport.Next,weinitializeourmiddleware:
varzmqm=newZmqMiddlewareManager(reply);
zmqm.use(middleware.zlib());
zmqm.use(middleware.json());
WecreatedanewZmqMiddlewareManagerobjectandthenaddedtwomiddlewares,oneforcompressing/decompressingthemessagesandanotheroneforparsing/serializingJSONmessages.
NoteForbrevity,wedidnotshowtheimplementationofthezlibmiddleware,butyoucanfinditinthesamplecodethatisdistributedwiththebook.
Nowwearereadytohandlearequestcomingfromtheclient,wewilldothisbysimplyaddinganothermiddleware,thistimeusingitasarequesthandler:
zmqm.use({
inbound:function(message,next){
console.log('Received:',message.data);
if(message.data.action==='ping'){
this.send({action:'pong',echo:message.data.echo});
}
next();
}
});
Sincethislastmiddlewareisdefinedafterthezlibandjsonmiddlewares,wecantransparentlyusethedecompressedanddeserializedmessagethatisavailableinthemessage.datavariable.Ontheotherhand,anydatapassedtosend()willbeprocessedbytheoutboundmiddleware,whichinourcasewillserializethencompressthedata.
Theclient
Ontheclientsideofourlittleapplication,'client.js',wewillfirsthavetoinitiateanewØMQ'req'(request)socketconnectedtotheport5000,theoneusedbyourserver:
varzmq=require('zmq');
varZmqMiddlewareManager=require('./zmqMiddlewareManager');
varmiddleware=require('./middleware');
varrequest=zmq.socket('req');
request.connect('tcp://127.0.0.1:5000');
Then,weneedtosetupourmiddlewareframeworkinthesamewaythatwedidfortheserver:
varzmqm=newZmqMiddlewareManager(request);
zmqm.use(middleware.zlib());
zmqm.use(middleware.json());
Next,wecreateaninboundmiddlewaretohandletheresponsescomingfromtheserver:
zmqm.use({
inbound:function(message,next){
console.log('Echoedback:',message.data);
next();
}
});
Intheprecedingcode,wesimplyinterceptanyinboundresponseandprintittotheconsole.
Finally,wesetupatimertosendsomepingrequestsatregularintervals,alwaysusingthezmqMiddlewareManagertogetalltheadvantagesofourmiddleware:
setInterval(function(){
zmqm.send({action:'ping',echo:Date.now()});
},1000);
Wecannowtryourapplicationbyfirststartingtheserver:
nodeserver
Wecanthenstarttheclientwiththefollowingcommand:
nodeclient
Atthispoint,weshouldseetheclientsendingmessagesandtheserverechoingthemback.
Ourmiddlewareframeworkdiditsjob;itallowedustodecompress/compressanddeserialize/serializeourmessagestransparently,leavingthehandlersfreetofocuson
theirbusinesslogic!
CommandAnotherdesignpatternwithhugeimportanceinNode.jsisCommand.Initsmostgenericdefinition,wecanconsideracommandasanyobjectthatencapsulatesalltheinformationnecessarytoperformanactionatalatertime.So,insteadofinvokingamethodorafunctiondirectly,wecreateanobjectrepresentingtheintentiontoperformsuchaninvocation;itwillthenbetheresponsibilityofanothercomponenttomaterializetheintent,transformingitintoanactualaction.Traditionally,thispatternisbuiltaroundfourmajorcomponents,asshowninthefollowingfigure:
ThetypicalorganizationoftheCommandpatterncanbedescribedasfollows:
Command:Thisistheobjectencapsulatingtheinformationnecessarytoinvokeamethodorfunction.Client:ThiscreatestheCommandandprovidesittotheInvoker.Invoker:ThisisresponsibleforexecutingtheCommandontheTarget.Target(orReceiver):Thisisthesubjectoftheinvocation.Itcanbealonefunctionorthemethodofanobject.
Aswewillsee,thesefourcomponentscanvaryalotdependingonthewaywewanttoimplementthepattern;thisshouldnotsoundnewatthispoint.
UsingtheCommandpattern,insteadofdirectlyexecutinganoperation,hasseveraladvantagesandapplications:
Acommandcanbescheduledforexecutionatalatertime.Acommandcanbeeasilyserializedandsentoverthenetwork.Thissimplepropertyallowsustodistributejobsacrossremotemachines,transmitcommandsfromthebrowsertotheserver,createRPCsystems,andsoon.Commandsmakeiteasytokeepahistoryofalltheoperationsexecutedonasystem.Commandsareanimportantpartofsomealgorithmsfordatasynchronizationandconflictresolution.Acommandscheduledforexecutioncanbecancelledifit'snotyetexecuted.It
canalsobereverted(undone),bringingthestateoftheapplicationtothepointbeforethecommandwasexecuted.Severalcommandscanbegroupedtogether.Thiscanbeusedtocreateatomictransactionsortoimplementamechanismwherebyalltheoperationsinthegroupareexecutedatonce.Differentkindsoftransformationscanbeperformedonasetofcommandssuchasduplicateremoval,joiningandsplitting,orapplyingmorecomplexalgorithmssuchasOperationalTransformation(OT),whichisthebaseformostoftoday'sreal-timecollaborativesoftwaresuchascollaborativetextediting.
TipAgreatexplanationofhowOperationalTransformationworkscanbefoundatwww.codecommit.com/blog/java/understanding-and-applying-operational-transformation.
Theprecedinglistclearlyshowsushowimportantthispatternis,especiallyinaplatformlikeNode.js,wherenetworkingandasynchronousexecutionareessentialplayers.
AflexiblepatternAswealreadymentioned,thecommandpatterninJavaScriptcanbeimplementedinmanydifferentways;wearenowgoingtodemonstrateonlyafewofthemjusttogiveanideaofitsscope.
ThetaskpatternWecanstartoffwiththemostbasicandtrivialimplementation:thetaskpattern.TheeasiestwayinJavaScripttocreateanobjectrepresentinganinvocationis,ofcourse,creatingaclosure:
functioncreateTask(target,args){
returnfunction(){
target.apply(null,args);
}
}
Thisshouldnotlooknewatall;wehaveusedthispatternalreadysomanytimesalongthebook,andinparticular,inChapter2,AsynchronousControlFlowPatterns.Thistechniqueallowedustouseaseparatecomponenttocontrolandschedulethe
executionofourtasks,whichisessentiallyequivalenttotheinvokeroftheCommandpattern.Forexample,doyourememberhowweweredefiningtaskstopasstotheasynclibrary?Orevenbetter,doyourememberhowwewereusingthunksincombinationwithgenerators?ThecallbackpatternitselfcanbeconsideredaverysimpleversionoftheCommandpattern.
AmorecomplexcommandLet'snowworkonanexampleofamorecomplexcommand;thistimewewanttosupportundoandserialization.Let'sstartwiththetargetofourcommands,alittleobjectthatisresponsibleforsendingstatusupdatestoaservicelikeTwitter.Weuseamockofsuchaserviceforsimplicity:
varstatusUpdateService={
statusUpdates:{},
sendUpdate:function(status){
console.log('Statussent:'+status);
varid=Math.floor(Math.random()*1000000);
statusUpdateService.statusUpdates[id]=status;
returnid;
},
destroyUpdate:function(id){
console.log('Statusremoved:'+id);
deletestatusUpdateService.statusUpdates[id];
}
}
Now,let'screateacommandtorepresentthepostingofanewstatusupdate:
functioncreateSendStatusCmd(service,status){
varpostId=null;
varcommand=function(){
postId=service.sendUpdate(status);
};
command.undo=function(){
if(postId){
service.destroyUpdate(postId);
postId=null;
}
};
command.serialize=function(){
return{type:'status',action:'post',status:status};
}
returncommand;
}
TheprecedingfunctionisafactorythatproducesnewsendStatuscommands.Eachcommandimplementsthefollowingthreefunctionalities:
1. Thecommanditselfisafunctionthatwheninvokedwilltriggertheaction;inotherwords,itimplementsthetaskpatternthatwehaveseenbefore.Thecommandwhenexecutedwillsendanewstatusupdateusingthemethodsofthetargetservice.
2. Anundo()function,attachedtothemaintask,thatrevertstheeffectsoftheoperations.Inourcase,wearesimplyinvokingthedestroyUpdate()methodonthetargetservice.
3. Aserialize()functionthatbuildsaJSONobjectthatcontainsallthenecessaryinformationtoreconstructthesamecommandobject.
Afterthis,wecanbuildanInvoker;wecanstartbyimplementingitsconstructoranditsrun()method:
functionInvoker(){
this.history=[];
}
Invoker.prototype.run=function(cmd){
this.history.push(cmd);
cmd();
console.log('Commandexecuted',cmd.serialize());
};
Therun()methodthatwedefinedearlieristhebasicfunctionalityofourInvoker;itisresponsibleforsavingthecommandintothehistoryinstancevariableandthentriggeringtheexecutionofthecommanditself.Next,wecanaddanewmethodthatdelaystheexecutionofacommand:
Invoker.prototype.delay=function(cmd,delay){
varself=this;
setTimeout(function(){
self.run(cmd);
},delay)
}
Then,wecanimplementanundo()methodthatrevertsthelastcommand:
Invoker.prototype.undo=function(){
varcmd=this.history.pop();
cmd.undo();
console.log('Commandundone',cmd.serialize());
}
Finally,wealsowanttobeabletorunacommandonaremoteserver,byserializingandthentransferringitoverthenetworkusingawebservice:
Invoker.prototype.runRemotely=function(cmd){
varself=this;
request.post('http://localhost:3000/cmd',
{json:cmd.serialize()},function(err){
console.log('Commandexecutedremotely',cmd.serialize());
});
}
NowthatwehavetheCommand,theInvoker,andtheTarget,theonlycomponentmissingistheClient.Let'sstartfrominstantiatingtheInvoker:
varinvoker=newInvoker();
Then,wecancreateacommandusingthefollowinglineofcode:
varcommand=createSendStatusCmd(statusUpdateService,'HI!');
Wenowhaveacommandrepresentingthepostingofastatusmessage;wecanthendecidetodispatchitimmediately:
invoker.run(command);
Oops,wemadeamistake;let'sreverttothestateofourtimeline,asitwasbeforesendingthelastmessage:
invoker.undo();
Wecanalsodecidetoschedulethemessagetobesentinahourfromnow:
invoker.delay(command,1000*60*60);
Alternatively,wecandistributetheloadoftheapplicationbymigratingthetasktoanothermachine:
invoker.runRemotely(command);
Thelittleexamplethatwehavejustcreatedshowshowwrappinganoperationinacommandcanopenaworldofpossibilities,andthat'sjustthetipoftheiceberg.
Asthelastremarks,itisworthnoticingthatafull-fledgedCommandpatternhasbe
usedonlywhenreallyneeded.Wesaw,infact,howmuchadditionalcodewehadtowritetosimplyinvokeamethodofstatusUpdateService;ifallthatweneedisonlyaninvocation,thenacomplexcommandwouldbeoverkill.Ifhowever,weneedtoscheduletheexecutionofataskorrunanasynchronousoperation,thenthesimplertaskpatternoffersthebestcompromise.Ifinstead,weneedmoreadvancedfeaturessuchasundosupport,transformations,conflictresolution,oroneoftheotherfancyusecasesthatwedescribedpreviously,usingamorecomplexrepresentationforthecommandisalmostnecessary.
SummaryInthischapter,welearnedhowsomeofthetraditionalGoFdesignpatternscanbeappliedtoJavaScriptand,inparticular,totheNode.jsphilosophy.Someofthemweretransformed,someweresimplified,otherrenamedoradaptedaspartoftheirassimilationbythelanguage,theplatform,andthecommunity.WeemphasizedhowsimplepatternssuchasFactorycangreatlyimprovetheflexibilityofourcodeandhowwithProxy,Decorator,andAdapterwecanmanipulate,extend,andadapttheinterfaceofexistingobjects.Strategy,State,andTemplate,instead,haveshownushowtosplitabiggeralgorithmintoitsstaticandvariableparts,allowingustoimprovethecodereuseandextensibilityofourcomponents.BylearningtheMiddlewarepattern,wearenowabletoprocessourdatausingasimple,extensible,andelegantparadigm.Finally,theCommandpatternprovideduswithasimpleabstractiontomakeanyoperationmoreflexibleandpowerful.
WealsoacquiredotherevidenceofhowJavaScriptisaboutgettingthingsdoneandbuildingsoftwarebycomposingdifferentreusableobjectsorfunctionsinsteadofextendingmanylittleclassesorinterfaces.Also,fordeveloperscomingfromotherObject-Orientedlanguages,itmighthavelookedweirdtoseehowdifferentsomedesignpatternsbecomewhenimplementedinJavaScript;somemightfeellostknowingthattheremightbenotjustonebutrathermanydifferentwaysforimplementingadesignpattern.
JavaScriptisapragmaticlanguage,wesaid;itallowsustogetthingsdonequickly;however,withoutanykindofstructureorguideline,weareaskingfortrouble.That'swherethisbookand,inparticular,thischaptercomesuseful.Theytrytoteachtherightbalancebetweencreativityandrigor,byshowingthattherearepatternsthatcanbereusedtoimproveourcodebutalsothattheirimplementationisnotthemostimportantdetail,asitcanvaryalotorevenoverlapwithotherpatterns;whatreallymattersisinsteadtheblueprint,theguideline,theideaatthebaseofthepattern.ThisistherealreusablepieceofinformationthatwecanexploittodesignbetterNode.jsapplicationswithbothfunandmethod.
Inthenextchapter,wewillanalyzesomemoredesignpatternsbyfocusingononeofthemostopinionatedaspectsofprogramming:howtoorganizeandconnectmodulestogether.
Chapter5.WiringModulesTheNode.jsmodulesystembrilliantlyfillsanoldgapintheJavaScriptlanguage:thelackofanativewayoforganizingcodeintodifferentself-containedunits.Oneofitsbiggestadvantagesistheabilitytolinkthesemodulestogetherusingtherequire()function(aswehaveseeninChapter1,Node.jsDesignFundamentals),asimpleyetpowerfulapproach.However,manydevelopersnewtoNode.jsmightfindthisconfusing;oneofthemostaskedquestionsisinfact:what'sthebestwaytopassaninstanceofcomponentXintomoduleY?
Sometimes,thisconfusionresultsinadesperatequestfortheSingletonpatterninthehopeoffindingamorefamiliarwaytolinkourmodulestogether.Ontheotherside,somemightoverusetheDependencyInjectionpattern,leveragingittohandleanytypeofdependency(evenstateless)withoutaparticularreason.ItshouldnotbesurprisingthattheartofmodulewiringisoneofthemostcontroversialandopinionatedtopicsinNode.js.Therearemanyschoolsofthoughtinfluencingthisarea,butnoneofthemcanbeconsideredtopossesstheundisputedtruth.Everyapproach,infact,hasitsprosandconsandtheyoftenendupmixedtogetherinthesameapplication,adapted,customized,orusedindisguiseunderothernames.
Inthischapter,we'regoingtoanalyzethevariousapproachesforwiringmodulesandhighlighttheirstrengthsandweaknesses,sothatwecanrationallychooseandmixthemtogetherdependingonthebalancebetweensimplicity,reusability,andextensibilitythatwewanttoobtain.Inparticular,we'regoingtopresentthemostimportantpatternsrelatedtothistopic,whichareasfollows:
HardcodeddependencyDependencyinjectionServicelocatorDependencyinjectioncontainers
Wewillthenexploreacloselyrelatedproblem,namely,howtowireplugins.Thiscanbeconsideredaspecializationofmodulewiringanditmostlypresentsthesametraits,butthecontextofitsapplicationisslightlydifferentandpresentsitsownchallenges,especiallywhenapluginisdistributedasaseparateNode.jspackage.Wewilllearnthemaintechniquestocreateaplugin-capablearchitectureandwewillthenfocusonhowtointegratethesepluginsintotheflowofthemainapplication.
Attheendofthischapter,theobscureartofNode.jsmodulewiringshouldnotbeamysterytousanymore.
ModulesanddependenciesEverymodernapplicationistheresultoftheaggregationofseveralcomponentsand,astheapplicationgrows,thewayweconnectthesecomponentsbecomesawinorlosefactor.It'snotonlyaproblemrelatedtotechnicalaspectssuchasextensibility,butit'salsoaconcernwiththewayweperceivethesystem.Atangleddependencygraphisaliabilityanditaddstothetechnicaldebtoftheproject;insuchasituation,anychangeinthecodeaimedtoeithermodifyorextenditsfunctionalitycanresultinatremendouseffort.
Intheworstcase,thecomponentsaresotightlyconnectedtogetherthatitbecomesimpossibletoaddorchangeanythingwithoutrefactoringorevencompletelyrewritingentirepartsoftheapplication.This,ofcourse,doesnotmeanthatwehavetoover-engineerourdesignstartingfromtheveryfirstmodule,butsurelyfindingagoodbalancefromtheverybeginningcanmakeahugedifference.
Node.jsprovidesagreattoolfororganizingandwiringthecomponentsofanapplicationtogether:it'stheCommonJSmodulesystem.However,themodulesystemaloneisnotaguaranteeforsuccess;ifononeside,itaddsaconvenientlevelofindirectionbetweentheclientmoduleandthedependency,thenontheother,itmightintroduceatightercouplingifnotusedproperly.Inthissection,wewilldiscusssomefundamentalaspectsofdependencywiringinNode.js.
ThemostcommondependencyinNode.jsInsoftwarearchitecture,wecanconsideranyentity,state,ordataformat,whichinfluencesthebehaviororstructureofacomponent,asadependency.Forexample,acomponentmightusetheservicesofferedbyanothercomponent,relyonaparticularglobalstateofthesystem,orimplementaspecificcommunicationprotocolinordertoexchangeinformationwithothercomponents,andsoon.Theconceptofdependencyisverybroadandsometimeshardtoevaluate.
InNode.js,though,wecanimmediatelyidentifyoneessentialtypeofdependency,whichisthemostcommonandeasytoidentify;ofcourse,wearetalkingaboutthedependencybetweenmodules.Modulesarethefundamentalmechanismatourdisposaltoorganizeandstructureourcode;it'sunreasonabletobuildalargeapplicationwithoutrelyingonthemodulesystematall.Ifusedproperlytogroupthevariouselementsofanapplication,itcanbringalotofadvantages.Infact,thepropertiesofamodulecanbesummedupasfollows:
Amoduleismorereadableandunderstandablebecause(ideally)it'smorefocused
Beingrepresentedasaseparatefile,amoduleiseasiertoidentifyAmodulecanbemoreeasilyreusedacrossdifferentapplications
Amodulerepresentstheperfectlevelofgranularityforperforminginformationhidingandgivesaneffectivemechanismtoexposeonlythepublicinterfaceofacomponent(usingmodule.exports).
However,simplyspreadingthefunctionalityofanapplicationoralibraryacrossdifferentmodulesisnotenoughforasuccessfuldesign;ithastobedoneright.Oneofthefallaciesisendingupinasituationwheretherelationshipbetweenourmodulesbecomessostrongwecreateauniquemonolithicentity,whereremovingorreplacingamodulewouldreverberateacrossmostofthearchitecture.Weareimmediatelyabletorecognizethatthewayweorganizeourcodeintomodulesandthewayweconnectthemtogether,playastrategicrole.Andaswithanyprobleminsoftwaredesign,it'samatteroffindingtherightbalancebetweendifferentmeasures.
CohesionandCouplingThetwomostimportantpropertiestobalancewhenbuildingmodulesarecohesionandcoupling.Theycanbeappliedtoanytypeofacomponentorsubsysteminsoftwarearchitecture,sowecanusethemasguidelineswhenbuildingNode.jsmodulesaswell.Thesetwopropertiescanbedefinedasfollows:
Cohesion:Thisisameasureofthecorrelationbetweenthefunctionalitiesofacomponent.Forexample,amodulethatdoesonlyonething,whereallitspartscontributetothatonesingletaskhasahighcohesion.Amodulethatcontainsfunctionstosaveanytypeofobjectintoadatabase—saveProduct(),saveInvoice(),saveUser(),andsoon—hasalowcohesion.Coupling:Thismeasureshowmuchacomponentisdependentontheothercomponentsofasystem.Forexample,amoduleistightlycoupledtoanothermodulewhenitdirectlyreadsormodifiesthedataoftheothermodule.Also,twomodulesthatinteractviaaglobalorsharedstatearetightlycoupled.Ontheotherside,twomodulesthatcommunicateonlyviathepassingofparametersarelooselycoupled.
Thedesirablescenarioistohaveahighcohesionandaloosecoupling,whichusuallyresultsinmoreunderstandable,reusable,andextensiblemodules.
StatefulmodulesInJavaScript,everythingisanobject.Wedon'thaveabstractconceptssuchaspureinterfacesorclasses;itsdynamictypingalreadyprovidesanaturalmechanismto
decoupletheinterface(orpolicy)fromtheimplementation(ordetail).That'soneofthereasonswhysomeofthedesignpatternsthatwehaveseeninChapter4,DesignPatterns,lookedsodifferentandsimplifiedcomparedtotheirtraditionalimplementation.
InJavaScript,wehaveminimalproblemsinseparatinginterfacesfromimplementations;however,bysimplyusingtheNode.jsmodulesystem,wearealreadyintroducingahardcodedrelationshipwithoneparticularimplementation.Undernormalconditions,thereisnothingwrongwiththis,butifweuserequire()toloadamodulethatexportsastatefulinstance,suchasadbhandle,anHTTPserverinstance,theinstanceofaservice,oringeneralanyobjectwhichisnotstateless,weareactuallyreferencingsomethingverysimilartoaSingleton,thusinheritingitsprosandcons,withtheadditionofsomecaveats.
TheSingletonpatterninNode.jsAlotofpeoplenewtoNode.jsgetconfusedabouthowtoimplementtheSingletonpatterncorrectly,mostofthetimewiththesimpleintentofsharinganinstanceacrossthevariousmodulesofanapplication.But,theanswerinNode.jsiseasierthanwhatwemightthink;simplyexportinganinstanceusingmodule.exportsisalreadyenoughtoobtainsomethingverysimilartotheSingletonpattern.Consider,forexample,thefollowinglineofcode:
//'db.js'module
module.exports=newDatabase('my-app-db');
Bysimplyexportinganewinstanceofourdatabase,wecanalreadyassumethatwithinthecurrentpackage(whichcaneasilybetheentirecodeofourapplication),wearegoingtohaveonlyoneinstanceofthedbmodule.Thisispossiblebecause,asweknow,Node.jswillcachethemoduleafterthefirstinvocationofrequire(),makingsuretonotexecuteitagainatanysubsequentinvocation,returninginsteadthecachedinstance.Forexample,wecaneasilyobtainasharedinstanceofthedbmodulethatwedefinedearlier,withthefollowinglineofcode:
vardb=require('./db');
Butthereisacaveat;themoduleiscachedusingitsfullpathaslookupkey,thereforeitisguaranteedtobeaSingletononlywithinthecurrentpackage.WesawinChapter1,Node.jsDesignFundamentals,thateachpackagemighthaveitsownsetofprivatedependenciesinsideitsnode_modulesdirectory,whichmightresultinmultipleinstancesofthesamepackageandthereforeofthesamemodule,withtheresultthatourSingletonmightnotbesingleanymore.Consider,forexample,thecasewherethedbmoduleiswrappedintoapackagenamedmydb.Thefollowinglinesofcodewill
beinitspackage.jsonfile:
{
"name":"mydb",
"main":"db.js"
}
Nowconsiderthefollowingpackagedependencytree:
app/
`--node_modules
|--packageA
|`--node_modules
|`--mydb
`--packageB
`--node_modules
`--mydb
BothpackageAandpackageBhaveadependencyonthemydbpackage;inturn,theapppackage,whichisourmainapplication,dependsonpackageAandpackageB.Thescenariowejustdescribed,willbreaktheassumptionabouttheuniquenessofthedatabaseinstance;infact,bothpackageAandpackageBwillloadthedatabaseinstanceusingacommandsuchasthefollowing:
vardb=require('mydb');
However,packageAandpackageBwillactuallyloadtwodifferentinstancesofourpretendingsingleton,becausethemydbmodulewillresolvetoadifferentdirectorydependingonthepackageitisrequiredfrom.
Atthispoint,wecaneasilysaythattheSingletonpattern,asdescribedintheliterature,doesnotexistinNode.jsunlesswedon'tusearealglobalvariabletostoreit,somethingsuchasthefollowing:
global.db=newDatabase('my-app-db');
Thiswouldguaranteethattheinstancewillbeonlyoneandsharedacrosstheentireapplication,andnotjustthesamepackage.However,thisisapracticetoavoidatallcosts;mostofthetime,wedon'treallyneedapureSingleton,andanyway,aswewillseelater,thereareotherpatternsthatwecanusetoshareaninstanceacrossthedifferentpackages.
Note
Throughoutthisbook,forsimplicity,wewillusethetermSingletontodescribeastatefulobjectexportedbyamodule,evenifthisdoesn'trepresentarealsingletoninthestrictdefinitionoftheterm.Wecansurelysaythough,thatitsharesthesamepracticalintentwiththeoriginalpattern:toeasilyshareastateacrossdifferentcomponents.
PatternsforwiringmodulesNowthatwehavediscussedsomebasictheoryarounddependenciesandcoupling,wearereadytodiveintosomemorepracticalconcepts.Inthissection,infact,wearegoingtopresentthemainmodulewiringpatterns.Ourfocuswillbemainlypointedtowardsthewiringofstatefulinstances,whichare,withoutanydoubts,themostimportanttypeofdependenciesinanapplication.
HardcodeddependencyWestartouranalysisbylookingatthemostconventionalrelationshipbetweentwomodules,whichisthehardcodeddependency.InNode.js,thisisobtainedwhenaclientmoduleexplicitlyloadsanothermoduleusingrequire().Aswewillseeinthissection,thiswayofestablishingmoduledependenciesissimpleandeffective,butwehavetopayadditionalattentiontohardcodingdependencieswithstatefulinstances,asthiswouldlimitthereusabilityofourmodules.
BuildinganauthenticationserverusinghardcodeddependenciesLet'sstartouranalysisbylookingatthestructurerepresentedbythefollowingfigure:
Theprecedingfigureshowsatypicalexampleoflayeredarchitecture;itdescribesthestructureofasimpleauthenticationsystem.AuthControlleracceptstheinputfromtheclient,extractsthelogininformationfromtherequest,andperformssomepreliminaryvalidation.ItthenreliesonAuthServicetocheckwhethertheprovidedcredentialsmatchwiththeinformationstoredinthedatabase;thisisdonebyexecutingsomespecificqueriesusingaDBhandleasameanstocommunicatewiththedatabase.Thewaythesethreecomponentsareconnectedtogetherwilldeterminetheirlevelofreusability,testability,andmaintainability.
ThemostnaturalwaytowirethesecomponentstogetherisrequiringtheDBmodulefromAuthServiceandthenrequiringAuthServicefromAuthController.Thisisthehardcodeddependencythatwearetalkingabout.
Let'sdemonstratethisinpracticebyactuallyimplementingthesystemthatwejustdescribed.Let'sthendesignasimpleauthenticationserver,whichexposesthe
followingtwoHTTPAPIs:
POST'/login':ThisreceivesaJSONobjectthatcontainsausernameandpasswordpairtoauthenticate.Onsuccess,itreturnsaJSONWebToken(JWT),whichcanbeusedinsubsequentrequeststoverifytheidentityoftheuser.
NoteJSONWebTokenisaformatforrepresentingandsharingclaimsbetweenparties.ItspopularityisgrowingwiththeexplosionofSinglePageApplicationsandCross-originresourcesharing(CORS),asamoreflexiblealternativetocookie-basedauthentication.ToknowmoreaboutJWT,youcanrefertoitsspecification(currentlyindraft)athttp://self-issued.info/docs/draft-ietf-oauth-json-web-token.html.
GET'/checkToken':ThisreadsatokenfromaGETqueryparameterandverifiesitsvalidity.
Forthisexample,wearegoingtouseseveraltechnologies;someofthemarenotnewtous.Inparticular,wearegoingtouseexpress(https://npmjs.org/package/express)toimplementtheWebAPI,andlevelup(https://npmjs.org/package/levelup)tostoretheuser'sdata.
Thedbmodule
Let'sstartbybuildingourapplicationfromthebottomup;theveryfirstthingweneedisamodulethatexposesalevelUpdatabaseinstance.Let'sdothisbycreatinganewfilenamedlib/db.jsandincludingthefollowingcontent:
varlevel=require('level');
varsublevel=require('level-sublevel');
module.exports=sublevel(
level('example-db',{valueEncoding:'json'})
);
TheprecedingmodulesimplycreatesaconnectiontoaLevelDBdatabasestoredinthe./example-dbdirectory,thenitdecoratestheinstanceusingthesublevelplugin(https://npmjs.org/package/level-sublevel),whichaddsthesupporttocreateandqueryseparatesectionsofthedatabase(itcanbecomparedtoaSQLtableorMongoDBcollection).Theobjectexportedbythemoduleisthedatabasehandle
itself,whichisastatefulinstance,therefore,wearecreatingasingleton.
TheauthServicemodule
Nowthatwehavethedbsingleton,wecanuseittoimplementthelib/authService.jsmodule,whichisthecomponentresponsibleforcheckingauser'scredentialsagainsttheinformationinthedatabase.Thecodeisasfollows(onlytherelevantpartsareshown):
[...]
vardb=require('./db');
varusers=db.sublevel('users');
vartokenSecret='SHHH!';
exports.login=function(username,password,callback){
users.get(username,function(err,user){
[...]
});
};
exports.checkToken=function(token,callback){
[...]
users.get(userData.username,function(err,user){
[...]
});
};
TheauthServicemoduleimplementsthelogin()service,whichisresponsibleforcheckingausername/passwordpairagainsttheinformationinthedatabase,andthecheckToken()service,whichtakesinatokenandverifiesitsvalidity.
Theprecedingcodealsoshowsthefirstexampleofahardcodeddependencywithastatefulmodule.Wearetalkingaboutthedbmodule,whichweloadbysimplyrequiringit.Theresultingdbvariablecontainsanalreadyinitializeddatabasehandlethatwecanusestraightawaytoperformourqueries.
Atthispoint,wecanseethatallthecodethatwecreatedfortheauthServicemoduledoesnotreallynecessitateoneparticularinstanceofthedbmodule—anyinstancewillsimplywork.However,wehardcodedthedependencytooneparticulardbinstance,andthismeansthatwewillbeunabletoreuseauthServiceincombinationwithanotherdatabaseinstancewithouttouchingitscode.
TheauthControllermodule
Continuingtogoupinthelayersoftheapplication,wearenowgoingtoseewhatthelib/authController.jsmodulelookslike.Thismoduleisresponsibleforhandlingthe
HTTPrequestsandit'sessentiallyacollectionoftheexpressroutes;themodule'scodeisthefollowing:
varauthService=require('./authService');
exports.login=function(req,res,next){
authService.login(req.body.username,req.body.password,
function(err,result){
[...]
}
);
};
exports.checkToken=function(req,res,next){
authService.checkToken(req.query.token,
function(err,result){
[...]
}
);
};
TheauthControllermoduleimplementstwoexpressroutes:oneforperformingtheloginandreturningthecorrespondingauthenticationtoken(login()),andanotherforcheckingthevalidityofthetoken(checkToken()).BoththeroutesdelegatemostoftheirlogictoauthService,sotheironlyjobistodealwiththeHTTPrequestandresponse.
Wecanseethat,inthiscasetoo,wearehardcodingthedependencywithastatefulmodule,authService.Yes,theauthServicemoduleisstatefulbytransitivity,becauseitdependsdirectlyonthedbmodule.Withthis,weshouldstarttounderstandhowahardcodeddependencycaneasilypropagateacrossthestructureoftheentireapplication:theauthControllermoduledependsontheauthServicemodule,whichinturndependsonthedbmodule;transitively,thismeansthattheauthServicemoduleitselfisindirectlylinkedtooneparticulardbinstance.
Theappmodule
Finally,wecanputallthepiecestogetherbyimplementingtheentrypointoftheapplication.Followingtheconvention,wewillplacethislogicinamodulenamedapp.jssittingintherootofourproject,asfollows:
varexpress=require('express');
varbodyParser=require('body-parser');
varerrorHandler=require('errorhandler');
varhttp=require('http');
varauthController=require('./lib/authController');
varapp=module.exports=express();
app.use(bodyParser.json());
app.post('/login',authController.login);
app.get('/checkToken',authController.checkToken);
app.use(errorHandler());
http.createServer(app).listen(3000,function(){
console.log('Expressserverstarted');
});
Aswecansee,ourappmoduleisreallybasic;itcontainsasimpleexpressserver,whichregisterssomemiddlewareandthetworoutesexportedbyauthController.Ofcourse,themostimportantlineofcodeforusiswherewerequireauthControllertocreateahardcodeddependencywithitsstatefulinstance.
Runningtheauthenticationserver
Beforewecantrytheauthenticationserverthatwejustimplemented,weadviseyoutopopulatethedatabasewithsomesampledatausingthepopulate_db.jsscript,whichisprovidedinthecodesamples.Afterdoingthis,wecanfireuptheserverbyrunningthefollowingcommand:
nodeapp
Wecanthentrytoinvokethetwowebservicesthatwecreated;wecanuseaRESTclienttodothisoralternativelythegoodoldcurlcommand.Forexample,toexecutealogin,wecanrunthefollowingcommand:
curl-XPOST-d'{"username":"alice","password":"secret"}'
http://localhost:3000/login-H"Content-Type:
application/json"
Theprecedingcommandshouldreturnatokenthatwecanusetotestthe/checkLoginwebservice(justreplace<TOKEN>inthefollowingcommand):
curl-XGET-H"Accept:application/json"
http://localhost:3000/checkToken?token=<TOKENHERE>
Theprecedingcommandshouldreturnastringsuchasthefollowing,whichconfirmsthatourserverisworkingasexpected:
{"ok":"true","user":{"username":"alice"
}}
Prosandconsofhardcodeddependencies
Thesamplewejustimplemented,demonstratedtheconventionalwayofwiringmodulesinNode.js,leveragingthefullpowerofitsmodulesystemtomanagethedependenciesbetweenthevariouscomponentsoftheapplication.Weexportedstatefulinstancesfromourmodules,lettingNode.jsmanagetheirlifecycle,andthenwerequiredthemdirectlyfromotherpartsoftheapplication.Theresultisanimmediatelyintuitiveorganization,easytounderstandanddebug,whereeachmoduleinitializesandwiresitselfwithoutanyexternalintervention.
Ontheotherside,however,hardcodingthedependencyonastatefulinstancelimitsthepossibilityofwiringthemoduleagainstotherinstances,whichmakesitlessreusableandhardertounittest.Forexample,reusingauthServiceincombinationwithanotherdatabaseinstancewouldbeclosetoimpossible,asitsdependencyishardcodedwithoneparticularinstance.Similarly,testingauthServiceinisolationcanbeadifficulttask,becausewecannoteasilymockthedatabaseusedbythemodule.
Asalastconsideration,it'simportanttoseethatmostofthedisadvantagesofusinghardcodeddependenciesareassociatedwithstatefulinstances.Thismeansthatifweuserequire()toloadastatelessmodule,forexample,afactory,constructor,orasetofstatelessfunctions,wedon'tincurthesamekindofproblems.Wewillstillhaveatightcouplingwithaspecificimplementation,butinNode.js,thisusuallydoesnotimpactthereusabilityofacomponent,asitdoesnotintroduceacouplingwithaparticularstate.
DependencyinjectionThedependencyinjection(DI)patternisprobablyoneofthemostmisunderstoodconceptsinsoftwaredesign.ManyassociatethetermwithframeworksanddependencyinjectioncontainerssuchasSpring(forJavaandC#)orPimple(forPHP),butinrealityitisamuchsimplerconcept.Themainideabehindthedependencyinjectionpatternisthedependenciesofacomponentbeingprovidedasinputbyanexternalentity.
Suchanentitycanbeaclientcomponentoraglobalcontainer,whichcentralizesthewiringofallthemodulesofthesystem.Themainadvantageofthisapproachisanimproveddecoupling,especiallyformodulesdependingonstatefulinstances.UsingDI,eachdependency,insteadofbeinghardcodedintothemodule,isreceivedfromtheoutside.Thismeansthatthemodulecanbeconfiguredtouseanydependencyandthereforecanbereusedindifferentcontexts.
Todemonstratethispatterninpractice,wearenowgoingtorefactortheauthenticationserverthatwebuiltintheprevioussection,usingdependencyinjectiontowireitsmodules.
RefactoringtheauthenticationservertousedependencyinjectionRefactoringourmodulestousedependencyinjectioninvolvestheuseofaverysimplerecipe:insteadofhardcodingthedependencytoastatefulinstance,wewillinsteadcreateafactory,whichtakesasetofdependenciesasarguments.
Let'sstartimmediatelywiththisrefactoring;let'sworkonthelib/db.jsmodulegivenasfollows:
varlevel=require('level');
varsublevel=require('level-sublevel');
module.exports=function(dbName){
returnsublevel(
level(dbName,{valueEncoding:'json'})
);
};
Thefirststepinourrefactoringprocessistotransformthedbmoduleintoafactory.Theresultisthatwecannowuseittocreateasmanydatabaseinstancesaswewant;thismeansthattheentiremoduleisnowreusableandstateless.
Let'smoveonandimplementthenewversionofthelib/authService.jsmodule:
varjwt=require('jwt-simple');
varbcrypt=require('bcrypt');
module.exports=function(db,tokenSecret){
varusers=db.sublevel('users');
varauthService={};
authService.login=function(username,password,callback){
//...sameasinthepreviousversion
};
authService.checkToken=function(token,callback){
//...sameasinthepreviousversion
};
returnauthService;
};
Also,theauthServicemoduleisnowstateless;itdoesn'texportanyparticularinstanceanymore,justasimplefactory.Themostimportantdetailthoughisthatwemadethedbdependencyinjectableasanargumentofthefactoryfunction,removingwhatpreviouslywasahardcodeddependency.Thissimplechangeenablesusto
createanewauthServicemodulebywiringittoanydatabaseinstance.
Wecanrefactorthelib/authController.jsmoduleinasimilarwayasfollows:
module.exports=function(authService){
varauthController={};
authController.login=function(req,res,next){
//...sameasinthepreviousversion
};
authController.checkToken=function(req,res,next){
//...sameasinthepreviousversion
};
returnauthController;
};
TheauthControllermoduledoesnothaveanyhardcodeddependencyatall,notevenstateless!Theonlydependency,theauthServicemodule,isprovidedasinputtothefactoryatthemomentofitsinvocation.
Okay,nowit'stimetoseewhereallthesemodulesareactuallycreatedandwiredtogether;theanswerliesintheapp.jsmodule,whichrepresentsthetopmostlayerinourapplication;itscodeisthefollowing:
[...]
vardbFactory=require('./lib/db');//[1]
varauthServiceFactory=require('./lib/authService');
varauthControllerFactory=require('./lib/authController');
vardb=dbFactory('example-db');//[2]
varauthService=authServiceFactory(db,'SHHH!');
varauthController=authControllerFactory(authService);
app.post('/login',authController.login);//[3]
app.get('/checkToken',authController.checkToken);
[...]
Thepreviouscodecanbesummedupasfollows:
1. Firstly,weloadthefactoriesofourservices;atthispoint,theyarestillstatelessobjects.
2. Next,weinstantiateeachservicebyprovidingthedependenciesitrequires.Thisisthephasewhereallthemodulesarecreatedandwired.
3. Finally,weregistertheroutesoftheauthControllermodulewiththeexpressserveraswewouldnormallydo.
Ourauthenticationserverisnowwiredusingdependencyinjectionandreadytobeusedagain.
ThedifferenttypesofdependencyinjectionTheexamplewejustpresenteddemonstratedonlyonetypeofdependencyinjection(factoryinjection),butthereareacouplemoreworthmentioning:
Constructorinjection:InthistypeofDI,thedependenciesarepassedtoaconstructoratthemomentofitscreation;onepossibleexamplecanbethefollowing:
varservice=newService(dependencyA,dependencyB);
Propertyinjection:InthistypeofDI,thedependenciesareattachedtoanobjectafteritscreation,asdemonstratedbythefollowingcode:
varservice=newService();//worksalsowithafactory
service.dependencyA=anInstanceOfDependencyA;
Propertyinjectionimpliesthatanobjectiscreatedinaninconsistentstate,becauseit'snotwiredtoitsdependencies,soit'stheleastrobust,butsometimesitmightbeusefulwhentherearecyclesbetweenthedependencies.Forexample,ifwehavetwocomponents,AandB,bothusingfactoryorconstructorinjectionandbothdependingoneachother,wecannotinstantiateeitherofthembecausebothwouldrequiretheothertoexistinordertobecreated.Let'sconsiderasimpleexample,asfollows:
functionAfactory(b){
return{
foo:function(){
b.say();
},
what:function(){
return'Hello!';
}
}
}
functionBfactory(a){
return{
a:a,
say:function(){
console.log('Isay:'+a.what);
}
}
}
Thedependencydeadlockbetweenthetwoprecedingfactoriescanberesolvedonlyusingpropertyinjection,forexample,byfirstcreatinganincompleteinstanceofB,whichthencanbeusedtocreateA.Finally,wewillinjectAintoBbysettingtherelativepropertyasfollows:
varb=Bfactory(null);
vara=Afactory(b);
a.b=b;
NoteInsomerarecircumstances,acycleinthedependencygraphisnoteasilyavoidable;however,itisimportanttorememberthatoftenitisasymptomofbaddesign.
ProsandconsofdependencyinjectionIntheauthenticationserverexample,usingdependencyinjectionwewereabletodecoupleourmodulesfromaparticulardependencyinstance.Theresultisthatwecannowreuseeachmodulewithminimaleffortandwithoutanychangeintheircode.Testingamodulethatusesthedependencyinjectionpatternisalsogreatlysimplified;wecaneasilyprovidemockeddependenciesandtestourmodulesinisolationfromthestateoftherestofthesystem.
Anotherimportantaspecttobehighlightedfromtheexamplewepresentedearlieristhatweshiftedthedependencywiringresponsibilityfromthebottomtothetopofourarchitecture.Theideaisthathigh-levelcomponentsarebynaturelessreusablethanlow-levelcomponents,andthat'sbecausethemorewegoupinthelayersofanapplicationthemoreacomponentbecomesspecific.
Startingfromthisassumptionwecanthenunderstandthattheconventionalwaytoseeanapplicationarchitecture,wherehigh-levelcomponentsowntheirlower-leveldependencies,canbeinverted,sothatthelower-levelcomponentsdependonlyonaninterface(inJavaScript,it'sjusttheinterfacethatweexpectfromadependency),whiletheownershipofdefiningtheimplementationofadependencyisgiventothehigher-levelcomponents.Inourauthenticationserver,infact,allthedependenciesareinstantiatedandwiredinthetopmostcomponent,theappmodule,whichisalsothelessreusableandsoisthemostexpendableintermsofcoupling.
Alltheseadvantagesintermsofdecouplingandreusability,though,comewithapricetopay.Ingeneral,theinabilitytoresolveadependencyatcodingtimemakesitmoredifficulttounderstandtherelationshipbetweenthevariouscomponentsofasystem.
Also,ifwelookatthewayweinstantiatedallthedependenciesintheappmodule,wecanseethatwehadtofollowaspecificorder;wepracticallyhadtomanuallybuildthedependencygraphoftheentireapplication.Thiscanbecomeunmanageablewhenthenumberofmodulestowirebecomeshigh.
Aviablesolutiontothisproblemistosplitthedependencyownershipbetweenmultiplecomponents,insteadofhavingitcentralizedallinoneplace.Thiscanreducethecomplexityinvolvedinmanagingthedependenciesexponentially,aseachcomponentwouldberesponsibleonlyforitsparticulardependencysubgraph.Ofcourse,wecanalsochoosetousedependencyinjectiononlylocally,justwhennecessary,insteadofbuildingtheentireapplicationontopofit.
Wewillseelaterinthechapterthat,anotherpossiblesolutiontosimplifythewiringofmodulesincomplexarchitecturesistouseadependencyinjectioncontainer,acomponentexclusivelyresponsibleforinstantiatingandwiringallthedependenciesofanapplication.
Usingdependencyinjectionsurelyincreasesthecomplexityandverbosityofourmodules,butaswesawearlier,therearemanygoodreasonsfordoingthis.Itisuptoustochoosetherightapproach,dependingonthebalancebetweensimplicityandreusabilitythatwewanttoobtain.
NoteDependencyinjectionisoftenmentionedincombinationwiththeDependencyInversionprincipleandInversionofControl;however,theyallaredifferentconcepts(eventhoughcorrelated).
ServicelocatorIntheprevioussection,welearnedhowdependencyinjectioncanliterallytransformthewaywewireourdependencies,obtainingreusableanddecoupledmodules.AnotherpatternwithaverysimilarintentisServiceLocator.Itscoreprincipleistohaveacentralregistryinordertomanagethecomponentsofthesystemandtoactasamediatorwheneveramoduleneedstoloadadependency.Theideaistoasktheservicelocatorforthedependency,insteadofhardcodingit,asshowninthefollowingimage:
Itisimportanttounderstandthatbyusingaservicelocatorweareintroducingadependencyonit,sothewaywewireittoourmodulesdeterminestheirlevelofcouplingandthereforetheirreusability.InNode.js,wecanidentifythreetypesofservicelocators,dependingonthewaytheyarewiredtothevariouscomponentsofthesystem:
HardcodeddependencyonservicelocatorInjectedservicelocatorGlobalservicelocator
Thefirstisdefinitelytheoneofferingtheleastadvantagesintermsofdecoupling,asitconsistsofdirectlyreferencingtheinstanceoftheservicelocatorusingrequire().InNode.js,thiscanbeconsideredananti-patternbecauseitintroducesatightcouplingwiththecomponentsupposedlymeanttoprovideabetterdecoupling.Inthiscontext,aservicelocatorclearlydoesnotprovideanyvalueintermsofreusability,butitonlyaddsanotherlevelofindirectionandcomplexity.
Ontheotherside,aninjectedservicelocatorisreferencedbyacomponentthroughdependencyinjection.Thiscanbeconsideredamoreconvenientwayforinjectinganentiresetofdependenciesatonce,insteadofprovidingthemonebyone,butaswewillsee,itsadvantagesdonotendhere.
Thethirdwayofreferencingaservicelocatorisdirectlyfromtheglobalscope.Thishasthesamedisadvantagesasthatofthehardcodedservicelocator,butsinceitisglobal,itisarealsingletonandthereforeitcanbeeasilyusedasapatternforsharinginstancesbetweenpackages.Wewillseehowthisworkslaterinthechapter,butfornowwecancertainlysaythatthereareveryfewreasonsforusingaglobalservicelocator.
Tip
TheNode.jsmodulesystemalreadyimplementsavariationoftheservicelocatorpattern,withrequire()representingtheglobalinstanceoftheservicelocatoritself.
Alltheconsiderationsdiscussedherewillbemoreclearoncewestartusingtheservicelocatorpatterninarealexample.Let'sthenrefactortheauthenticationserveragaintoapplywhatwelearned.
RefactoringtheauthenticationservertouseaservicelocatorWearenowgoingtoconverttheauthenticationservertouseaninjectedservicelocator.Todothis,thefirststepistoimplementtheservicelocatoritself;wewilluseanewmodule,'lib/serviceLocator.js':
module.exports=function(){
vardependencies={};
varfactories={};
varserviceLocator={};
serviceLocator.factory=function(name,factory){//[1]
factories[name]=factory;
};
serviceLocator.register=function(name,instance){//[2]
dependencies[name]=instance;
};
serviceLocator.get=function(name){//[3]
if(!dependencies[name]){
varfactory=factories[name];
dependencies[name]=factory&&factory(serviceLocator);
if(!dependencies[name]){
thrownewError('Cannotfindmodule:'+name);
}
}
returndependencies[name];
};
returnserviceLocator;
};
OurserviceLocatormoduleisafactoryreturninganobjectwiththreemethods:
factory()isusedtoassociateacomponentnameagainstafactory.register()isusedtoassociateacomponentnamedirectlywithaninstance.get()retrievesacomponentbyitsname.Ifaninstanceisalreadyavailable,it
simplyreturnsit;otherwise,ittriestoinvoketheregisteredfactorytoobtainanewinstance.Itisveryimportanttoobservethatthemodulefactoriesareinvokedbyinjectingthecurrentinstanceoftheservicelocator(serviceLocator).Thisisthecoremechanismofthepatternthatallowsthedependencygraphforoursystemtobebuiltautomaticallyandon-demand.Wewillseehowthisworksinamoment.
NoteAsimplepattern,closelyresemblingaservicelocator,istouseanobjectasanamespaceforasetofdependencies:
vardependencies={};
vardb=require('./lib/db');
varauthService=require('./lib/authService');
dependencies.db=db();
dependencies.authService=authService(dependencies);
Let'snowconvertthe'lib/db.js'modulestraightawaytodemonstratehowourserviceLocatorworks:
varlevel=require('level');
varsublevel=require('level-sublevel');
module.exports=function(serviceLocator){
vardbName=serviceLocator.get('dbName');
returnsublevel(
level(dbName,{valueEncoding:'json'})
);
}
Thedbmoduleusestheservicelocatorreceivedininputtoretrievethenameofthedatabasetoinstantiate.Thisisaninterestingpointtohighlight;aservicelocatorcanbeusednotonlytoreturncomponentinstancesbutalsotoprovideconfigurationparametersthatdefinethebehavioroftheentiredependencygraphthatwewanttocreate.
Thenextstepistoconvertthe'lib/authService.js'module:
[...]
module.exports=function(serviceLocator){
vardb=serviceLocator.get('db');
vartokenSecret=serviceLocator.get('tokenSecret');
varusers=db.sublevel('users');
varauthService={};
authService.login=function(username,password,callback){
//...sameasinthepreviousversion
}
authService.checkToken=function(token,callback){
//...sameasinthepreviousversion
}
returnauthService;
};
Also,theauthServicemoduleisafactorythattakestheservicelocatorastheinput.Thetwodependenciesofthemodule,thedbhandleandtokenSecret(whichisanotherconfigurationparameter)areretrievedusingtheget()methodoftheservicelocator.
Inasimilarway,wecanconvertthe'lib/authController.js'module:
module.exports=function(serviceLocator){
varauthService=serviceLocator.get('authService');
varauthController={};
authController.login=function(req,res,next){
//...sameasinthepreviousversion
};
authController.checkToken=function(req,res,next){
//...sameasinthepreviousversion
};
returnauthController;
}
Nowwearereadytoseehowtheservicelocatorisinstantiatedandconfigured.Thishappensofcourse,inthe'app.js'module:
[...]
varsvcLoc=require('./lib/serviceLocator')();//[1]
svcLoc.register('dbName','example-db');//[2]
svcLoc.register('tokenSecret','SHHH!');
svcLoc.factory('db',require('./lib/db'));
svcLoc.factory('authService',require('./lib/authService'));
svcLoc.factory('authController',
require('./lib/authController'));
varauthController=svcLoc.get('authController');//[3]
app.post('/login',authController.login);
app.all('/checkToken',authController.checkToken);
[...]
Thisishowthewiringworksusingournewservicelocator:
1. Weinstantiateanewservicelocatorbyinvokingitsfactory.2. Weregistertheconfigurationparametersandmodulefactoriesagainstthe
servicelocator.Atthispoint,allourdependenciesarenotinstantiatedyet;wejustregisteredtheirfactories.
3. WeloadauthControllerfromtheservicelocator;thisistheentrypointthattriggerstheinstantiationoftheentiredependencygraphofourapplication.WhenweaskfortheinstanceoftheauthControllercomponent,theservicelocatorinvokestheassociatedfactorybyinjectinganinstanceofitself,thentheauthControllerfactorywilltrytoloadtheauthServicemodule,whichinturninstantiatesthedbmodule.
It'sinterestingtoseethelazynatureoftheservicelocator;eachinstanceiscreatedonlywhenneeded.Butthereisanotherimportantimplication;wecansee,infact,thateverydependencyisautomaticallywiredwithouttheneedtomanuallydoitinadvance.Theadvantageisthatwedon'thavetoknowinadvancewhattherightorderforinstantiatingandwiringthemodulesis,itallhappensautomaticallyandon-demand.Thisismuchmoreconvenientcomparedtothesimpledependencyinjectionpattern.
TipAnothercommonpatternistouseanexpressserverinstanceasasimpleservicelocator.ThiscanbeachievedusingexpressApp.set(name,instance)toregisteraserviceandexpressApp.get(name)tothenretrieveit.Theconvenientpartofthispatternisthattheserverinstance,whichactsasaservicelocator,isalreadyinjectedintoeachmiddlewareandisaccessiblethroughtherequest.appproperty.Youcanfindanexampleofthispatterninthesamplesdistributedwiththebook.
ProsandconsofaservicelocatorServicelocatoranddependencyinjectionhavealotincommon;bothshiftthedependencyownershiptoanentityexternaltothecomponent.Butthewaywewiretheservicelocatordeterminestheflexibilityofourentirearchitecture.Itisnotbychancethatwechoseaninjectedservicelocatortoimplementourexample,as
opposedtoahardcodedorglobalservicelocator.Theselasttwovariationsalmostnullifytheadvantagesofthispattern.Infact,theresultwouldbethat,insteadofcouplingacomponentdirectlytoitsdependenciesusingrequire(),wewouldbecouplingittooneparticularinstanceoftheservicelocator.It'salsotruethatahardcodedservicelocatorwillstillgivemoreflexibilityinconfiguringwhatcomponenttoassociatewithaparticularname,butthisdoesstillnotgiveanybigadvantageintermsofreusability.
Also,likedependencyinjection,usingaservicelocatormakesithardertoidentifytherelationshipbetweenthecomponents,astheyareresolvedatruntime.Butinadditionitalsomakesitmoredifficulttoknowexactlywhatdependencyaparticularcomponentisgoingtorequire.Withdependencyinjectionthisisexpressedinamuchclearerway,bydeclaringthedependenciesinthefactoryorconstructorarguments.Withaservicelocator,thisismuchlessclearandwouldrequireacodeinspectionoranexplicitstatementinthedocumentationexplainingwhatdependenciesaparticularcomponentwilltrytoload.
Asafinalnote,itisimportanttoknowthatoftenaservicelocatorisincorrectlymistakenforadependencyinjectioncontainerbecauseitsharesthesameroleofserviceregistrywithit;however,thereisabigdifferencebetweenthetwo.Withaservicelocator,eachcomponentloadsitsdependenciesexplicitlyfromtheservicelocatoritself,whenusingaDIcontainerinstead,thecomponenthasnoknowledgeofthecontainer.
Thedifferencebetweenthesetwoapproachesisnoticeablefortworeasons:
Reusability:Acomponentrelyingonaservicelocatorislessreusablebecauseitrequiresthataservicelocatorisavailableinthesystem.Readability:Aswehavealreadysaid,aservicelocatorobfuscatesthedependencyrequirementsofacomponent.
Intermsofreusability,wecansaythattheservicelocatorpatternsitsinbetweenhardcodeddependenciesandDI.Intermsofconvenienceandsimplicity,itisdefinitelybetterthanmanualdependencyinjection,aswedon'thavetomanuallytakecareofbuildingtheentiredependencygraph.
Undertheseassumptions,aDIcontainerdefinitelyoffersthebestcompromiseintermsofreusabilityofthecomponentsandconvenience.Wearegoingtobetteranalyzethispatterninthenextsection.
DependencyinjectioncontainerThesteptotransformaservicelocatorintoaDependencyInjectioncontaineris
notbig,butaswealreadymentioned,itmakesahugedifferenceintermsofdecoupling.Withthispattern,infact,eachmoduledoesn'thavetodependontheservicelocatoranymore,butitcansimplyexpressitsneedintermsofdependenciesandtheDIcontainerwilldotherestseamlessly.Aswewillsee,thebigleapforwardofthismechanismisthateverymodulecanbereusedevenwithoutthecontainer.
DeclaringasetofdependenciestoaDIcontainerADIcontainerisessentiallyaservicelocatorwiththeadditionofonefeature:itidentifiesthedependencyrequirementsofamodulebeforeinstantiatingit.Forthistobepossible,amodulehastodeclareitsdependenciesinsomeway,andaswewillsee,wehavemultipleoptionsfordoingthis.
Thefirstandprobablythemostpopulartechniqueconsistsofinjectingasetofdependenciesbasedonthearguments'namesusedinafactoryorconstructor.Let'stake,forexample,theauthServicemodule:
module.exports=function(db,tokenSecret){
//...
}
Aswedefinedit,theprecedingmodulewillbeinstantiatedbyourDIcontainerusingthedependencieswithnamesdbandtokenSecret,averysimpleandintuitivemechanism.However,tobeabletoreadthenamesoftheargumentsofafunction,it'snecessarytousealittletrick.InJavaScript,wehavethepossibilitytoserializeafunction,obtainingatruntimeitssourcecode;thisisaseasyasinvokingtoString()onthefunctionreference.Then,withsomeregularexpressions,obtainingtheargumentslistiscertainlynotblackmagic.
TipThistechniqueofinjectingasetofdependenciesusingthenamesoftheargumentsofafunctionwaspopularizedbyAngularJS(http://angularjs.org),aclient-sideJavaScriptframeworkdevelopedbyGoogleandentirelybuiltontopofaDIcontainer.
Thebiggestproblemofthisapproachisthatitdoesn'tplaywellwithminification,apracticeusedextensivelyinclient-sideJavaScript,whichconsistsofapplyingparticularcodetransformationstoreducetotheminimumthesizeofthesourcecode.Manyminificatorsapplyatechniqueknownasnamemangling,whichessentiallyrenamesanylocalvariabletoreduceitslength,usuallytoasinglecharacter.Thebadnewsisthatfunctionargumentsarelocalvariablesandareusuallyaffectedbythis
process,causingthemechanismthatwedescribedfordeclaringdependenciestofallapart.Eventhoughminificationisnotreallynecessaryinserver-sidecode,it'simportanttoconsiderthatoftenNode.jsmodulesaresharedwiththebrowser,andthisisanimportantfactortoconsiderinouranalysis.
Luckily,aDIcontainermightuseothertechniquestoknowwhichdependenciestoinject.Thesetechniquesaregivenasfollows:
Wecanuseaspecialpropertyattachedtothefactoryfunction,forexample,anarrayexplicitlylistingallthedependenciestoinject:
module.exports=function(a,b){};
module.exports._inject=['db','another/dependency'];
Wecanspecifyamoduleasanarrayofdependencynamesfollowedbythefactoryfunction:
module.exports=['db','another/depencency',
function(a,b){}];
Wecanuseacommentannotationthatisappendedtoeachargumentofafunction(however,thisalsodoesn'tplaywellwithminification):
module.exports=function(a/*db*/,
b/*another/depencency*/){};
Allthesetechniquesarequiteopinionated,soforourexample,wearegoingtousethemostsimpleandpopular,whichistoobtainthedependencynamesusingtheargumentsofafunction.
RefactoringtheauthenticationservertouseaDIcontainerTodemonstratehowaDIcontainerismuchlessinvasivethanaservicelocator,wearenowgoingtorefactoragainourauthenticationserver,andtodosowearegoingtouseasstartingpointtheversioninwhichwewereusingtheplaindependencyinjectionpattern.Infact,whatwearegoingtodoisjustleaveuntouchedallthecomponentsoftheapplicationexceptfortheapp.jsmodule,whichisgoingtobethemoduleresponsibleforinitializingthecontainer.
Butfirst,weneedtoimplementourDIcontainer.Let'sdothatbycreatinganewmodulecalleddiContainer.jsunderthelib/directory.Thisisitsinitialpart:
varargsList=require('args-list');
module.exports=function(){
vardependencies={};
varfactories={};
vardiContainer={};
diContainer.factory=function(name,factory){
factories[name]=factory;
};
diContainer.register=function(name,dep){
dependencies[name]=dep;
};
diContainer.get=function(name){
if(!dependencies[name]){
varfactory=factories[name];
dependencies[name]=factory&&
diContainer.inject(factory);
if(!dependencies[name]){
thrownewError('Cannotfindmodule:'+name);
}
}
returndependencies[name];
};
//...tobecontinued
ThefirstpartofthediContainermoduleisfunctionallyidenticaltotheservicelocatorwehaveseenpreviously.Theonlynotabledifferencesare:
Werequireanewnpmmodulecalledargs-list(https://npmjs.org/package/args-list),whichwewillusetoextractthenamesoftheargumentsofafunction.Thistime,insteadofdirectlyinvokingthemodulefactory,werelyonanothermethodofthediContainermodulecalledinject(),whichwillresolvethedependenciesofamodule,andusethemtoinvokethefactory.
Let'sseehowthediContainer.inject()methodlookslike:
diContainer.inject=function(factory){
varargs=argsList(factory)
.map(function(dependency){
returndiContainer.get(dependency);
});
returnfactory.apply(null,args);
};
};//endofmodule.exports=function(){
TheprecedingmethodiswhatmakestheDIcontainerdifferentfromaservicelocator.Itslogicisverystraightforward:
1. Weextracttheargumentslistfromthefactoryfunctionwereceiveastheinput,
usingtheargs-listlibrary.2. Wethenmapeachargumentnametothecorrespondentdependencyinstance
retrievedusingtheget()method.3. Attheend,allwehavetodoisjustinvokethefactorybyprovidingthe
dependencylistthatwejustgenerated.
That'sreallyitforourdiContainer.Aswesawit'snotthatmuchdifferentfromaservicelocator,butthesimplestepofinstantiatingamodulebyinjectingitsdependenciesmakesadramaticdifference(ascomparedtoinjectingtheentireservicelocator).
Tocompletetherefactoringoftheauthenticationserver,wealsoneedtotweakthe'app.js'module:
[...]
vardiContainer=require('./lib/diContainer')();
diContainer.register('dbName','example-db');
diContainer.register('tokenSecret','SHHH!');
diContainer.factory('db',require('./lib/db'));
diContainer.factory('authService',
require('./lib/authService'));
diContainer.factory('authController',
require('./lib/authController'));
varauthController=diContainer.get('authController');
app.post('/login',authController.login);
app.get('/checkToken',authController.checkToken);
[...]
Aswecansee,thecodeoftheappmoduleisidenticaltotheonethatweusedtoinitializetheservicelocatorintheprevioussection.WecanalsonoticethattobootstraptheDIcontainer,andthereforetriggertheloadingoftheentiredependencygraph,westillhavetouseitasaservicelocatorbyinvokingdiContainer.get('authController').Fromthatpointon,everymoduleregisteredwiththeDIcontainerwillbeinstantiatedandwiredautomatically.
ProsandconsofaDependencyInjectioncontainerADIcontainerassumesthatourmodulesusethedependencyinjectionpatternandthereforeitinheritsmostofitsprosandcons.Inparticular,wehaveanimproveddecouplingandtestabilitybutontheothersidemorecomplexitybecauseourdependenciesareresolvedatruntime.ADIcontaineralsosharesmanypropertieswiththeservicelocatorpattern,butithasonitssidethefactthatitdoesn'tforcethemodulestodependonanyextraserviceexceptitsactualdependencies.Thisisa
hugeadvantagebecauseitallowseachmoduletobeusedevenwithouttheDIcontainer,usingasimplemanualinjection.
That'sessentiallywhatwedemonstratedinthissection:wetooktheversionoftheauthenticationserverwhereweusedtheplaindependencyinjectionpatternandthen,withoutmodifyinganyofitscomponents(exceptfortheappmodule),wewereabletoautomatizetheinjectionofeverydependency.
TipOnnpm,youcanfindalotofDIcontainerstoreuseortakeinspirationfromathttps://www.npmjs.org/search?q=dependency%20injection.
WiringpluginsThedreamarchitectureofasoftwareengineeristheonehavingasmall,minimalcore,extensibleasneededthroughtheuseofplugins.Unfortunately,thisisnotalwayseasytoobtain,sincemostofthetimeithasacostintermsoftime,resources,andcomplexity.Nonetheless,it'salwaysdesirabletosupportsomekindofexternalextensibility,eveniflimitedtojustsomepartsofthesystem.Inthissection,wearegoingtoplungeintothisfascinatingworldandfocusonadualisticproblem:
ExposingtheservicesofanapplicationtoapluginIntegratingapluginintotheflowoftheparentapplication
PluginsaspackagesOfteninNode.js,thepluginsofanapplicationareinstalledaspackagesintothenode_modulesdirectoryofaproject.Therearetwoadvantagesfordoingthis.Firstly,wecanleveragethepowerofnpmtodistributethepluginandmanageitsdependencies.Andsecondly,apackagecanhaveitsownprivatedependencygraph,whichreducesthechancesofhavingconflictsandincompatibilitiesbetweendependencies,asopposedtolettingthepluginusethedependenciesoftheparentproject.
Thefollowingdirectorystructuregivesanexampleofanapplicationwithtwopluginsdistributedaspackages:
application
'--node_modules
|--pluginA
'--pluginB
IntheNode.jsworldthisisaverycommonpractice,somepopularexamplesareexpress(http://expressjs.com)withitsmiddleware,gulp(http://gulpjs.com),grunt(http://gruntjs.com),nodebb(http://nodebb.org),anddocpad(http://docpad.org).
However,thebenefitsofusingpackagesarenotonlylimitedtoexternalplugins.Infact,onepopularpatternistobuildentireapplicationsbywrappingtheircomponentsintopackages,asiftheywereinternalplugins.So,insteadoforganizingthemodulesinthemainpackageoftheapplication,wecancreateaseparatepackageforeachbigchunkoffunctionalityandinstallitintothenode_modulesdirectory.
Tip
Apackagecanbeprivateandnotnecessarilyavailableonthepublicnpmregistry.Wecanalwayssetthe'private'flagintothe'package.json'topreventaccidentalpublicationtonpm.Then,wecancommitthepackagesintoaversioncontrolsystemsuchasgitorleverageaprivatenpmservertosharethemwiththerestoftheteam.
Whyfollowthispattern?Firstofallconvenience:peopleoftenfinditimpracticalortooverbosetoreferencethelocalmodulesofapackageusingtherelativepathnotation.Let's,forexample,considerthefollowingdirectorystructure:
application
|--componentA
|'--subdir
|'--moduleA
'--componentB
'--moduleB
IfwewanttoreferencemoduleBfrommoduleA,wehavetowritesomethinglikethis:
require('../../componentB/moduleB');
Instead,wecanleveragethepropertiesoftheresolvingalgorithmofrequire()(aswehavestudieditinChapter1,Node.jsDesignFundamentals)andputtheentirecomponentBdirectoryintoapackage.Byinstallingitintothenode_modulesdirectory,wecanthenwritesomethingsuchasthefollowing(fromanywhereinthemainpackageoftheapplication):
require('componentB/module');
Thesecondreasonforsplittingaprojectintopackagesisofcoursereusability.Apackagecanhaveitsownprivatedependenciesanditforcesthedevelopertothinkintermsofwhattoexposetothemainapplicationandwhatinsteadtokeepprivate,withbeneficialeffectsonthedecouplingandinformationhidingoftheentireapplication.
TipPattern:usepackagesasameanstoorganizeyourapplication,notjustfordistributingcodeincombinationwithnpm.
Theusecaseswehavejustdescribedmakeuseofapackagenotjustasa
stateless,reusablelibrary(likemostofthepackagesonnpm),butmoreasanintegralpartofaparticularapplication,providingservices,extendingitsfunctionalityormodifyingitsbehavior.Themaindifferenceisthatthesetypesofpackagesareintegratedinsideanapplicationratherthanjustused.
NoteForsimplicity,wewillusethetermplugintodescribeanypackagemeanttointegratewithaparticularapplication.
Aswewillsee,thecommonproblemthatwearegoingtofacewhendecidingtosupportthistypeofarchitectureisexposingpartsofthemainapplicationtoplugins.Infact,wecannotthinkofonlystatelessplugins—thisisofcourse,theaimforaperfectextensibility—butsometimesthepluginhastousesomeoftheservicesoftheparentapplicationinordertocarryoutitstasks.Thisaspectmightdependalotonthetechniqueusedtowiremodulesintheparentapplication.
ExtensionpointsThereareliterallyinfinitewaystomakeanapplicationextensible.Forexample,someofthedesignpatternswestudiedinChapter4,DesignPatternsaremeantexactlyforthis:usingProxyorDecoratorweareabletochangeoraugmentthefunctionalityofaservice;withStrategywecanswappartsofanalgorithm;withMiddlewarewecaninsertprocessingunitsinanexistingpipeline.Also,Streamscanprovidegreatextensibilitythankstotheircomposablenature.
Ontheotherhand,EventEmittersallowustodecoupleourcomponentsusingeventsandthepublish/subscribepattern.Anotherimportanttechniqueistoexplicitlydefineintheapplicationsomepointswherenewfunctionalitiescanbeattachedortheexistingonesmodified;thesepointsinanapplicationarecommonlyknownashooks.Tosummarize,themostimportantingredienttosupportpluginsisasetofextensionpoints.
Butalsothewaywewireourcomponentsplaysadecisiverolebecauseitcanaffectthewayweexposetheservicesoftheapplicationtotheplugin.Inthissection,wearemainlygoingtofocusonthisaspect.
Plugin-controlledvsApplication-controlledextension
Beforewegoaheadandpresentsomeexamples,itisimportanttounderstandthebackgroundofthetechniquewearegoingtouse.Therearemainlytwoapproachesforextendingthecomponentsofanapplication:
ExplicitextensionExtensionthroughInversionofControl(IoC)
Inthefirstcase,wehaveamorespecificcomponent(theoneprovidingthenewfunctionality)explicitlyextendingtheinfrastructure,whileinthesecondcase,itistheinfrastructuretocontroltheextensionbyloading,installing,orexecutingthenewspecificcomponent.Inthissecondscenario,theflowofcontrolisinverted,asshowninthefollowingimage:
InversionofControlisaverybroadprinciplethatcanbeappliednotonlytotheproblemofapplicationextensibility.Infact,inmoregeneraltermsitcanbesaidthatbyimplementingsomeformofIoC,insteadofthecustomcodecontrollingtheinfrastructure,theinfrastructurecontrolsthecustomcode.WithIoC,thevariouscomponentsofanapplicationtradeofftheirpowerofcontrollingtheflowinexchangeforanimprovedlevelofdecoupling.ThisisalsoknownastheHollywoodprincipleor"don'tcallus,we'llcallyou".
Forexample,adependencyinjectioncontainerisademonstrationoftheInversionofControlprincipleappliedtothespecificcaseofdependencymanagement.TheobserverpatternisanotherexampleofIoCappliedtostatemanagement.Template,Strategy,State,andMiddlewarearealsomorelocalizedmanifestationsofthesameprinciple.ThebrowserimplementstheIoCprinciplewhendispatchingUIeventstotheJavaScriptcode(it'snottheJavaScriptcodeactivelypollingthebrowserforevents).Andguesswhat,Node.jsitselffollowstheIoCprinciplewhencontrollingtheexecutionofthevariouscallbacksforus.
NoteToknowmoreabouttheInversionofControlprinciple,weadviseyoutostudythetopicdirectlyfromthewordsofitsmaster,MartinFowlerathttp://martinfowler.com/bliki/InversionOfControl.html.
Applyingthisconcepttothespecificcaseofpluginswecanthenidentifytwoformsofextension:
Plugin-controlledextensionApplication-controlledextension(IoC)
Inthefirstcase,itisthepluginthattapsintothecomponentsoftheapplicationtoextendthemasneeded,whileinthesecondcase,thecontrolisinthehandsoftheapplication,whichintegratesthepluginintooneofitsextensionpoints.
Tomakeaquickexample,let'sconsiderapluginthatextendstheexpressapplicationwithanewroute.Byusingaplugin-controlledextensionthiswouldlooklikethefollowing:
//intheapplication:
varapp=express();
require('thePlugin')(app);
//intheplugin:
module.exports=functionplugin(app){
app.get('/newRoute',function(req,res){...})
};
Ifinstead,wewanttouseanapplication-controlledextension(IoC),thesameprecedingexamplewouldlooklikethefollowing:
//intheapplication:
varapp=express();
varplugin=require('thePlugin')();
app[plugin.method](plugin.route,plugin.handler);
//intheplugin:
module.exports=functionplugin(){
return{
method:'get',
route:'/newRoute',
handler:function(req,res){...}
}
}
Inthelastcodefragment,wesawhowthepluginisonlyapassiveplayerintheextensionprocess;thecontrolisinthehandsoftheapplication,whichimplementstheframeworktoreceivetheplugin.
Basedontheprecedingexample,wecanimmediatelyidentifyafewimportantdifferencesbetweenthetwoapproaches:
Plugin-controlledextensionismorepowerfulandflexible,asoftenwehaveaccesstotheinternalsoftheapplicationandwecanmovefreelyasifthepluginwasactuallyapartoftheapplicationitself.However,thissometimescanbemorealiabilitythananadvantage.Infact,anychangeintheapplicationwouldmoreeasilyhaverepercussionsontheplugins,requiringconstantupdatesasthemainapplicationevolves.Application-controlledextensionrequiresaplugininfrastructureinthemainapplication.Withaplugin-controlledextension,theonlyrequirementisthatthecomponentsoftheapplicationareextensibleinsomeway.Withaplugin-controlledextension,itbecomesessentialtosharetheinternalservicesoftheapplicationwiththeplugin(intheprecedingsmallexample,theservicetosharewastheappinstance),otherwisewewouldnotbeabletoextendthem.Withanapplication-controlledextension,itmightstillbenecessarytoaccesssomeoftheservicesoftheapplication,nottoextendbutrathertousethem.Forexample,wemightwanttoquerythedbinstanceinourplugin,orleveragetheloggerofthemainapplication,justtonameafewscenarios.
Thislastpointshouldletusthinkabouttheimportanceofexposingtheservicesofanapplicationtothepluginandthat'swhatwearemainlyinterestedinexploring.Thebestwayofdoingthisistoshowapracticalexampleofaplugin-controlledextension,whichrequiresasmalleffortintermsofinfrastructureandwecanemphasizemoreontheproblemofsharingtheapplication'sstatewiththeplugins.
ImplementingalogoutpluginLet'snowstarttoworkonasmallpluginforourauthenticationserver.Withthewayweoriginallycreatedtheapplication,itisnotpossibletoexplicitlyinvalidateatoken,itsimplybecomesinvalidwhenitexpires.Now,wewanttoaddsupportforthisfeature,namelylogout,andwewanttodothatbynotmodifyingthecodeofthemainapplicationbutratherdelegatingthetasktoanexternalplugin.
Tosupportthisnewfeature,weneedtosaveeachtokeninthedatabaseafteritiscreatedandthencheckforitsexistenceeverytimewewanttovalidateit.Toinvalidateatoken,wesimplyneedtoremoveitfromthedatabase.
Todothis,wearegoingtouseaplugin-controlledextensiontoproxythecallsto
authService.login()andauthService.checkToken().WethenneedtodecoratetheauthServicewithanewmethodcalledlogout().Afterdoingthis,wealsowanttoregisteranewrouteagainstthemainexpressservertoexposeanewendpoint('/logout'),whichwecanusetoinvalidateatokenusinganHTTPrequest.
Wearegoingtoimplementthepluginwejustdescribedinfourdifferentvariations:
UsinghardcodeddependenciesUsingdependencyinjectionUsingaservicelocatorUsingadependencyinjectioncontainer
UsinghardcodeddependenciesThefirstvariationofthepluginwearenowgoingtoimplementcoversthecaseinwhichourapplicationmainlyuseshardcodeddependenciesforwiringitsstatefulmodules.Inthiscontext,ifourpluginlivesinapackageunderthenode_modulesdirectory,tousetheservicesofthemainapplicationwehavetogainaccesstotheparentpackage.Wecandothisintwoways:
Usingrequire()andnavigatingtotheapplication'srootusingrelativeorabsolutepaths.Usingrequire()byimpersonatingamoduleintheparentapplication—usuallythemoduleinstantiatingtheplugin.Thiswillallowustoeasilygainaccesstoalltheservicesoftheapplicationbyusingrequire(),asifitwasinvokedbytheparentapplicationandnotfromtheplugin.
Thefirsttechniqueislessrobustasitassumesthatthepackageisawareofthepositionofthemainapplication.Themoduleimpersonationpatterninstead,canbeusedregardlessofwherethepackageisrequiredfrom,andforthisreasonthisisthetechniquethatwearegoingtousetoimplementthenextdemo.
Tobuildourplugin,wefirstneedtocreateanewpackageunderthenode_modulesdirectory,namedauthsrv-plugin-logout.Beforewestartcoding,weneedtocreateaminimalpackage.jsontodescribethepackage,fillinginonlytheessentialparameters(thecompletepathtothefileis:node_modules/authsrv-plugin-logout/package.json):
{
"name":"authsrv-plugin-logout",
"version":"0.0.0"
}
Now,wearereadytocreatethemainmoduleofourplugin,wewillusethefile
'index.js'asitisthedefaultmodulethatNode.jswilltrytoloadwhenrequiringthepackage(ifno'main'propertyisdefinedinthe'package.json').Asalways,theinitiallinesofthemodulearededicatedtoloadingthedependencies;payattentiononhowwearegoingtorequirethem(file'node_modules/authsrv-plugin-logout/index.js'):
varparentRequire=module.parent.require;
varauthService=parentRequire('./lib/authService');
vardb=parentRequire('./lib/db');
varapp=parentRequire('./app');
vartokensDb=db.sublevel('tokens');
Thefirstlineofcodeiswhatmakesthedifference.Weobtainareferencetotherequire()functionoftheparentmodule,whichistheonethatloadstheplugin.Inourcasetheparentisgoingtobetheappmoduleinthemainapplication,andthismeansthateverytimeweuseparentRequire()weareloadingamoduleasifweweredoingitfrom'app.js'.
ThenextstepiscreatingaproxyfortheauthService.login()method.AfterstudyingthispatterninChapter4,DesignPatterns,weshouldalreadyknowhowitworks:
varoldLogin=authService.login;//[1]
authService.login=function(username,password,callback){
oldLogin(username,password,function(err,token){//[2]
if(err)returncallback(err);//[3]
tokensDb.put(token,{username:username},function(){
callback(null,token);
});
});
}
Intheprecedingcode,thestepsperformedareexplainedasfollows:
1. Wefirstsaveareferencetotheoldlogin()methodandthenweoverrideitwithourproxiedversion.
2. Intheproxyfunction,weinvoketheoriginallogin()methodbyprovidingacustomcallbacksothatwecanintercepttheoriginalreturnvalue.
3. Iftheoriginallogin()returnsanerror,wesimplyforwardittothecallback,otherwisewesavethetokenintothedatabase.
Similarly,weneedtointerceptthecallstocheckToken()sothatwecanaddourcustomlogic:
varoldCheckToken=authService.checkToken;
authService.checkToken=function(token,callback){
tokensDb.get(token,function(err,res){
if(err)returncallback(err);
oldCheckToken(token,callback);
});
}
Thistime,wefirstwanttocheckwhetherthetokenexistsinthedatabasebeforegivingthecontroltotheoriginalcheckToken()method.Ifthetokenisnotfound,theget()operationreturnsanerror;thismeansthatourtokenwasinvalidatedandsoweimmediatelyreturntheerrorbacktothecallback.
TofinalizetheextensionoftheauthService,wenowneedtodecorateitwithanewmethod,whichwewillusetoinvalidateatoken:
authService.logout=function(token,callback){
tokensDb.del(token,callback);
}
Thelogout()methodisverysimple:wejustdeletethetokenfromthedatabase.
Finally,wecanattachanewroutetotheexpressservertoexposethenewfunctionalitythroughawebservice:
app.get('/logout',function(req,res,next){
authService.logout(req.query.token,function(){
res.status(200).send({ok:true});
});
});
Now,ourpluginisreadytobeattachedtothemainapplication,sotodothiswejustneedtogobacktothemaindirectoryoftheapplicationandeditthe'app.js'module:
[...]
varapp=module.exports=express();
app.use(bodyParser.json());
require('authsrv-plugin-logout');
app.post('/login',authController.login);
app.all('/checkToken',authController.checkToken);
[...]
Aswecansee,toattachthepluginweonlyneedtorequireit.Assoonasthishappens—duringthestartupoftheapplication—theflowofcontrolisgiventotheplugin,whichinturnwillextendtheauthServiceandtheappmodules,aswesawearlier.
Nowourauthenticationserveralsosupportstheinvalidationofthetoken.Wedidthatinareusableway,thecoreoftheapplicationremainedalmostuntouched,andwewereabletoeasilyapplytheProxyandDecoratorpatternstoextenditsfunctionalities.
Wecannowtrytostarttheapplicationagain:
nodeapp
Then,wecanverifythatthenew/logoutwebserviceactuallyexistsandworksasexpected.Usingcurlwecannowtrytoobtainanewtokenusing/login:
curl-XPOST-d'{"username":"alice","password":"secret"}'
http://localhost:3000/login-H"Content-Type:
application/json"
Then,wecancheckwhetherthetokenisvalidusing/checkToken:
curl-XGET-H"Accept:application/json"
http://localhost:3000/checkToken?token=<TOKENHERE>
Then,wecanpassthetokentothe/logoutendpointtoinvalidateit;withcurlthiscanbedonewithacommandsuchasthis:
curl-XGET-H"Accept:application/json"
http://localhost:3000/logout?token=<TOKENHERE>
Now,ifwetrytocheckagainthevalidityofthetokenweshouldgetanegativeresponse,confirmingthatourpluginisworkingperfectly.
Evenwithasmallpluginliketheonewejustimplementedtheadvantagesofsupportingplugin-basedextensibilityareclear.Wealsolearnedhowtogainaccesstotheservicesofthemainapplicationfromanotherpackageusingthemoduleimpersonation.
NoteThemoduleimpersonationpatternisusedbyquiteafewNodeBBplugins;you
mightwanttocheckacoupleoftheminordertohaveanideaofhowthisisusedinarealapplication.Thesearethelinkstosomenotableexamples:
nodebb-plugin-poll:https://github.com/Schamper/nodebb-plugin-poll/blob/b4a46561aff279e19c23b7c635fda5037c534b84/lib/nodebb.jsnodebb-plugin-mentions:https://github.com/julianlam/nodebb-plugin-mentions/blob/9638118fa7e06a05ceb24eb521427440abd0dd8a/library.js#L4-13
Moduleimpersonationis,ofcourse,aformofhardcodeddependencyandshareswithitstrengthsandweaknesses.Fromoneside,itallowsustoaccessanyserviceofthemainapplicationwithlittleeffortandminimalinfrastructuralrequirements,butfromtheother,itcreatesatightcoupling,notonlywithaparticularinstanceofaservicebutalsowithitslocation,whichmoreeasilyexposestheplugintochangesandrefactoringsinthemainapplication.
ExposingservicesusingaservicelocatorSimilartomoduleimpersonation,theservicelocatorisalsoagoodchoiceifwewanttoexposeallthecomponentsofanapplicationtoitsplugins,butontopofthat,ithasamajoradvantage,becauseaplugincanusetheservicelocatortoexposeitsownservicestotheapplicationoreventootherplugins.
Let'snowrefactorourlogoutpluginagaintouseaservicelocator.We'llrefactorthemainmoduleoftheplugininthenode_modules/authsrv-plugin-logout/index.jsfile:
module.exports=function(serviceLocator){
varauthService=serviceLocator.get('authService');
vardb=serviceLocator.get('db');
varapp=serviceLocator.get('app');
vartokensDb=db.sublevel('tokens');
varoldLogin=authService.login;
authService.login=function(username,password,callback){
//...sameasinthepreviousversion
}
varoldCheckToken=authService.checkToken;
authService.checkToken=function(token,callback){
//...sameasinthepreviousversion
}
authService.logout=function(token,callback){
//...sameasinthepreviousversion
}
app.get('/logout',function(req,res,next){
//...sameasinthepreviousversion
});
};
Nowthatourpluginreceivestheservicelocatoroftheparentapplicationastheinput,itcanaccessanyofitsservicesasneeded.Thismeansthattheapplicationdoesnothavetoknowinadvancewhatthepluginisgoingtoneedintermsofdependencies;thisissurelyamajoradvantagewhenimplementingaplugin-controlledextension.
Thenextstepistoexecutethepluginfromthemainapplication,andtodothat,wehavetomodifytheapp.jsmodule.Wewillusetheversionoftheauthenticationserveralreadybasedontheservicelocatorpattern.Therequiredchangesaregiveninthefollowingblockofcode:
[...]
varsvcLoc=require('./lib/serviceLocator')();
svcLoc.register(...);
[...]
svcLoc.register('app',app);
varplugin=require('authsrv-plugin-logout');
plugin(svcLoc);
[...]
Thechangesarehighlightedintheprecedingcode;thosechangesenabledusto:
Registertheappmoduleitselfintheservicelocator,asthepluginmightwanttohaveaccesstoitRequirethepluginInvoketheplugin'smainfunctionbyprovidingtheservicelocatorasanargument
Aswealreadysaid,themainstrengthoftheservicelocatoristhatitprovidesasimplewaytoexposealltheservicesofanapplicationtoitsplugins,butitcanalsobeusedasamechanismforsharingservicesfromthepluginbackintotheparentapplicationorevenotherplugins.Thislastconsiderationisprobablythemainstrengthoftheservicelocatorpatterninthecontextofplugin-basedextensibility.
ExposingservicesusingdependencyinjectionUsingdependencyinjectiontopropagateservicestoapluginisaseasyasusingitintheapplicationitself.Thispatternbecomesalmostarequirementifit'salreadythemainmethodforwiringdependenciesintheparentapplication,butnothingpreventsusfromusingitwhentheprevalentformofdependencymanagementishardcodeddependenciesoraservicelocator.DIisalsoanidealchoicewhenwewantto
supportanapplication-controlledextension,becauseitprovidesbettercontroloverwhatissharedwiththeplugin.
Totesttheseassumptions,let'simmediatelytrytorefactorthelogoutplugintousedependencyinjection.Thechangesrequiredareminimal,solet'sstartfromthemainmoduleoftheplugin('node_modules/authsrv-plugin-logout/index.js'):
module.exports=function(app,authService,db){
vartokensDb=db.sublevel('tokens');
varoldLogin=authService.login;
authService.login=function(username,password,callback){
//...sameasinthepreviousversion
}
varoldCheckToken=authService.checkToken;
authService.checkToken=function(token,callback){
//...sameasinthepreviousversion
}
authService.logout=function(token,callback){
//...sameasinthepreviousversion
}
app.get('/logout',function(req,res,next){
//...sameasinthepreviousversion
});
};
Allwedidiswraptheplugin'scodeintoafactorythatreceivestheservicesoftheparentapplicationastheinput;therestofitremainsunchanged.
Tocompleteourrefactoring,wealsoneedtochangethewayweattachthepluginfromtheparentapplication;let'sthenchangethatonelinewherewerequirethepluginintheapp.jsmodule:
[...]
varplugin=require('authsrv-plugin-logout');
plugin(app,authService,authController,db);
[...]
Weintentionallydidn'tshowhowthesedependencieswereobtained.Infact,itdoesn'treallymakeanydifference,anymethodwillequallywork;wemightusehardcodeddependenciesorobtaintheinstancesfromfactoriesorfromaservicelocator,itdoesn'treallymatter.Thisprovesthatdependencyinjectionisaflexiblepatternwhenwiringpluginsthatcanbeusedregardlessofthewaywewiretheservicesintheparentapplication.
Butthedifferencesaremuchmoreprofound.Dependencyinjectionisdefinitelythecleanestwayofprovidingasetofservicestoaplugin,butmostimportantly,itoffersthebestlevelofcontroloverwhat'sexposedtoit,resultinginbetterinformationhidingandbetterprotectionagainsttooaggressiveextensions.However,thiscanbealsoconsideredadrawback,becausethemainapplicationcan'talwaysknowwhatservicesthepluginisgoingtoneed,soweendupeitherinjectingeveryservice,whichisimpractical,oronlyasubsetofthem,forexample,onlytheessentialcoreservicesoftheparentapplication.Forthisreason,dependencyinjectionisnottheidealchoiceifwemainlywanttosupportplugin-controlledextensibility;however,theuseofaDIcontainercaneasilysolvetheseissues.
NoteGrunt(http://gruntjs.com),ataskrunnerforNode.js,usesdependencyinjectiontoprovideeachpluginwithaninstanceofthecoregruntservice.Eachplugincanthenextenditbyattachingnewtasks,usingittoretrievetheconfigurationparameters,orrunningothertasks.Agruntpluginlookslikethefollowing:
module.exports=function(grunt){
grunt.registerMultiTask('taskName','description',
function(...){...}
);
};
ExposingservicesusingadependencyinjectioncontainerTakingthepreviousexampleasastartingpoint,wecanuseaDIcontainerincombinationwithourpluginbyapplyingasmallchangetotheappmodule,asshowninthefollowingcode:
[...]
vardiContainer=require('./lib/diContainer')();
diContainer.register(...);
[...]
//initializetheplugin
diContainer.inject(require('authsrv-plugin-logout'));
[...]
Afterregisteringthefactoriesortheinstancesofourapplication,allwehavetodoisinstantiatetheplugin,whichisdonebyinjectingitsdependenciesusingtheDIcontainer.Thisway,eachplugincanrequireitsownsetofdependencieswithoutthe
parentapplicationneedingtoknow.AllthewiringisagaincarriedoutautomaticallybytheDIcontainer.
UsingaDIcontaineralsomeansthateachplugincanpotentiallyaccessanyserviceoftheapplication,reducingtheinformationhidingandthecontroloverwhatcanbeusedorextended.ApossiblesolutiontothisproblemistocreateaseparateDIcontainerregisteringonlytheservicesthatwewanttoexposetoplugins;thiswaywecancontrolwhateachplugincanseeofthemainapplication.ThisdemonstratesthataDIcontainercanalsobeaverygoodchoiceintermsofencapsulationandinformationhiding.
Thisconcludesourlastrefactoringofthelogoutpluginandtheauthenticationserver.
SummaryThetopicofdependencywiringiscertainlyoneofthemostopinionatedinsoftwareengineering,butinthischapter,wetriedtokeeptheanalysisasfactualaspossibletogiveanobjectiveoverviewofthemostimportantwiringpatterns.WeclearedsomeofthemostcommondoubtsaroundSingletonsandinstancesinNode.js,andwelearnedhowtoconnectmodulesusinghardcodeddependencies,dependencyinjection,andservicelocators.Wepracticedeachtechniqueusingtheauthenticationserverasaplayground,allowingustoidentifytheprosandconsofeachapproach.
Inthesecondpartofthechapter,welearnedhowanapplicationcansupportplugins,butmostimportantly,howwecanwirethosepluginsintothemainapplication.Weappliedthesametechniquespresentedinthefirstpartofthechapter,butanalyzedthemfromanotherperspective.Wediscoveredhowimportantitcanbeforaplugintohaveaccesstotherightservicesofthemainapplication,andhowmuchthiscanimpactitscapabilities.
Bytheendofthischapter,weshouldfeelcomfortableinchoosingthebestapproachforthelevelofdecoupling,reusability,andsimplicitywewanttoobtaininourapplication.Wecanalsoconsiderusingmorethanonepatterninthesameapplication.Forexample,wecanusehardcodeddependenciesasthemaintechniqueandthenuseaservicelocatorwhenitcomestolinkingplugins;therearereallynolimitstowhatwecando,nowthatweknowthebestusecaseforeachapproach.
Sofarinthisbook,wehavefocusedouranalysisonhighlygenericandcustomizablepatterns,butfromthenextchapteronward,wewillshiftourattentiontosolvingmorespecifictechnicalproblems.Whatcomesnextis,infact,acollectionofrecipes,whichcanbeusedtosolvespecificissuesrelatedtoCPU-boundtasks,asynchronouscaching,andsharingcodewiththebrowser.
Chapter6.RecipesAlmostallthedesignpatternswe'veseensofarcanbeconsideredgenericandapplicabletomanydifferentareasofanapplication.Thereis,however,asetofpatternsthataremorespecificandfocusedonsolvingwell-definedproblems;wecancallthesepatternsrecipes.Asinreal-lifecooking,wehaveasetofwell-definedstepstofollowthatwillleadustoanexpectedoutcome.Ofcourse,thisdoesn'tmeanthatwecan'tusesomecreativitytocustomizetherecipestomatchthetasteofourguests,buttheoutlineoftheprocedureisusuallytheonethatmatters.Inthischapter,wearegoingtoprovidesomepopularrecipestosolvesomespecificproblemsweencounterinoureverydayNode.jsdevelopment.Theserecipesinclude:
RequiringmodulesthatareinitializedasynchronouslyBatchingandcachingasynchronousoperationstogetaperformanceboostinbusyapplications,usingonlyaminimaldevelopmenteffortRunningsynchronousCPU-boundoperationsthatcanblocktheeventloopandcrippletheabilityofNode.jstohandleconcurrentrequestsSharingcodewiththebrowser,theHolyGrailofNode.jsdevelopment
RequiringasynchronouslyinitializedmodulesInChapter1,Node.jsDesignFundamentals,whenwediscussedthefundamentalpropertiesoftheNode.jsmodulesystem,wementionedthefactthatrequire()workssynchronouslyandthatmodule.exportscannotbesetasynchronously.
ThisisoneofthemainreasonsfortheexistenceofsynchronousAPIinthecoremodulesandmanynpmpackages,theyareprovidedmoreasaconvenientalternative,tobeusedprimarilyforinitializationtasksratherthanasubstituteforasynchronousAPI.
Unfortunately,thisisnotalwayspossible;asynchronousAPImightnotalwaysbeavailable,especiallyforcomponentsusingthenetworkduringtheirinitializationphase,forexample,toperformhandshakeprotocolsortoretrieveconfigurationparameters.Thisisthecaseformanydatabasedriversandclientsformiddlewaresystemssuchasmessagequeues.
Canonicalsolutions
Let'stakeanexample:amodulecalleddb,whichconnectstoaremotedatabase.Thedbmodulewillbeabletoacceptrequestsonlyaftertheconnectionandthehandshakewiththeserverhavebeencompleted.Inthisscenario,weusuallyhavetwooptions:
Makingsurethatthemoduleisinitializedbeforestartingtouseit,otherwisewaitforitsinitialization.Thisprocesshastobedoneeverytimewewanttoinvokeanoperationontheasynchronousmodule:
vardb=require('aDb');//Theasyncmodule
module.exports=functionfindAll(type,callback){
if(db.connected){//isitinitialized?
runFind();
}else{
db.once('connected',runFind);
}
functionrunFind(){
db.findAll(type,callback);
});
};
UseDependencyInjectioninsteadofdirectlyrequiringtheasynchronousmodule.Bydoingthis,wecandelaytheinitializationofsomemodulesuntiltheirasynchronousdependenciesarefullyinitialized.Thistechniqueshiftsthecomplexityofmanagingthemoduleinitializationtoanothercomponent,usuallytheparentmodule.Inthefollowingexample,thiscomponentisapp.js:
//inthemoduleapp.js
vardb=require('aDb');//Theasyncmodule
varfindAllFactory=require('./findAll');
db.on('connected',function(){
varfindAll=findAllFactory(db);
});
//inthemodulefindAll.js
module.exports=function(db){
//dbisguaranteedtobeinitialized
returnfunctionfindAll(type,callback){
db.findAll(type,callback);
}
}
Wecanimmediatelyseethatthefirstoptioncanbecomehighlyundesirable,consideringtheamountofboilerplatecodeinvolved.
Also,thesecondoption,whichusesDependencyInjection,sometimesisundesirable,aswehaveseeninChapter5,WiringModules.Inbigprojects,itcanquicklybecomeover-complicated,especiallyifdonemanuallyandwithasynchronouslyinitializedmodules.TheseproblemswouldbemitigatedifwewereusingaDIcontainer
designedtosupportasynchronouslyinitializedmodules.
Aswewillseethough,thereisathirdalternativethatallowsustoeasilyisolatethemodulefromtheinitializationstateofitsdependencies.
PreinitializationqueuesAsimplepatterntodecoupleamodulefromtheinitializationstateofadependencyinvolvestheuseofqueuesandtheCommandpattern.Theideaistosavealltheoperationsreceivedbyamodulewhileit'snotyetinitializedandthenexecutethemassoonasalltheinitializationstepshavebeencompleted.
ImplementingamodulethatinitializesasynchronouslyTodemonstratethissimplebuteffectivetechnique,let'sbuildasmalltestapplication,nothingfancy,justsomethingtoverifyourassumptions.Let'sstartbycreatinganasynchronouslyinitializedmodulecalledasyncModule.js:
varasyncModule=module.exports;
asyncModule.initialized=false;
asyncModule.initialize=function(callback){
setTimeout(function(){
asyncModule.initialized=true;
callback();
},10000);
}
asyncModule.tellMeSomething=function(callback){
process.nextTick(function(){
if(!asyncModule.initialized){
returncallback(
newError('Idon\'thaveanythingtosayrightnow')
);
}
callback(null,'Currenttimeis:'+newDate());
});
}
Intheprecedingcode,asyncModuletriestodemonstratehowanasynchronouslyinitializedmoduleworks.Itexposesaninitialize()method,whichafteradelayof10seconds,setstheinitializedvariabletotrueandnotifiesitscallback(10secondsisalotforarealapplication,butforusit'sgreattohighlightanyracecondition).Theothermethod,tellMeSomething(),returnsthecurrenttime,butifthemoduleisnotyetinitialized,itgeneratesanerror.
Thenextstepistocreateanothermoduledependingontheservicewejustcreated.Let'sconsiderasimpleHTTPrequesthandlerimplementedinafilecalledroutes.js:
varasyncModule=require('./asyncModule');
module.exports.say=function(req,res){
asyncModule.tellMeSomething(function(err,something){
if(err){
res.writeHead(500);
returnres.end('Error:'+err.message);
}
res.writeHead(200);
res.end('Isay:'+something);
});
}
ThehandlerinvokesthetellMeSomething()methodofasyncModule,thenitwritestheresultintoanHTTPresponse.Aswecansee,wearenotperforminganycheckontheinitializationstateofasyncModule,andaswecanimagine,thiswilllikelyleadtoproblems.
Now,let'screateaverybasicHTTPserverusingnothingbutthecorehttpmodule(theapp.jsfile):
varhttp=require('http');
varroutes=require('./routes');
varasyncModule=require('./asyncModule');
asyncModule.initialize(function(){
console.log('Asyncmoduleinitialized');
});
http.createServer(function(req,res){
if(req.method==='GET'&&req.url==='/say'){
returnroutes.say(req,res);
}
res.writeHead(404);
res.end('Notfound');
}).listen(8000,function(){console.log('Started')});
Theprecedingsmallmoduleistheentrypointofourapplication,andallitdoesistriggertheinitializationofasyncModuleandcreateanHTTPserverthatmakesuseoftherequesthandlerwecreatedpreviously(routes.say()).
Wecannowtrytofireupourserverbyexecutingtheapp.jsmoduleasusual.Aftertheserverisstarted,wecantrytohittheURL,http://localhost:8000/say,withabrowserandseewhatcomesbackfromourasyncModule.
Asexpected,ifwesendtherequestjustaftertheserverisstarted,theresultwillbeanerrorasfollows:
Error:Idon'thaveanythingtosayrightnow
ThismeansthatasyncModuleisnotyetinitialized,butwestilltriedtouseit.Dependingontheimplementationdetailsoftheasynchronouslyinitializedmodule,wecouldhavereceivedagracefulerror,lostimportantinformation,orevencrashedtheentireapplication.Ingeneral,thesituationwejustdescribedhastoalwaysbeavoided.Mostofthetime,afewfailingrequestsmightnotbeaconcernortheinitializationmightbesofastthat,inpractice,itwouldneverhappen;however,forhighloadapplicationsandcloudserversdesignedtoautoscale,bothoftheseassumptionsmightquicklygetobliterated.
WrappingthemodulewithpreinitializationqueuesToaddrobustnesstoourserver,wearenowgoingtorefactoritbyapplyingthepatternwedescribedatthebeginningofthesection.WewillqueueanyoperationinvokedonasyncModuleduringthetimeit'snotyetinitializedandthenflushthequeueassoonwearereadytoprocessthem.ThislookslikeagreatapplicationfortheStatepattern!Wewillneedtwostates,onethatqueuesalltheoperationswhilethemoduleisnotyetinitialized,andanotherthatsimplydelegateseachmethodtotheoriginalasyncModulemodule,whentheinitializationiscomplete.
Often,wedon'thavethechancetomodifythecodeoftheasynchronousmodule;so,toaddourqueuinglayer,wewillneedtocreateaproxyaroundtheoriginalasyncModulemodule.
Let'sstarttoworkonthecode;let'screateanewfilenamedasyncModuleWrapper.jsandlet'sstartbuildingitpiecebypiece.Thefirstthingthatweneedtodoistocreatetheobjectthatdelegatestheoperationstotheactivestate:
varasyncModule=require('./asyncModule');
varasyncModuleWrapper=module.exports;
asyncModuleWrapper.initialized=false;
asyncModuleWrapper.initialize=function(){
activeState.initialize.apply(activeState,arguments);
};
asyncModuleWrapper.tellMeSomething=function(){
activeState.tellMeSomething.apply(activeState,arguments);
};
Intheprecedingcode,asyncModuleWrappersimplydelegateseachofitsmethodstothecurrentlyactivestate.Let'sthenseewhatthetwostateslooklike,startingfromnotInitializedState:
varpending=[];
varnotInitializedState={
initialize:function(callback){
asyncModule.initialize(function(){
asyncModuleWrapper.initalized=true;
activeState=initializedState;//[1]
pending.forEach(function(req){//[2]
asyncModule[req.method].apply(null,req.args);
});
pending=[];
callback();//[3]
});
},
tellMeSomething:function(callback){
returnpending.push({
method:'tellMeSomething',
args:arguments
});
}
};
Whentheinitialize()methodisinvoked,wetriggertheinitializationoftheoriginalasyncModulemodule,providingacallbackproxy.Thisallowsourwrappertoknowwhentheoriginalmoduleisinitializedandconsequentlytriggersthefollowingoperations:
1. UpdatestheactiveStatevariablewiththenextstateobjectinourflow—initializedState.
2. Executesallthecommandsthatwerepreviouslystoredinthependingqueue.3. Invokestheoriginalcallback.
Asthemoduleatthispointisnotyetinitialized,thetellMeSomething()methodofthisstatesimplycreatesanewCommandobjectandaddsittothequeueofthependingoperations.
Atthispoint,thepatternshouldalreadybeclear:whentheoriginalasyncModulemoduleisnotyetinitialized,ourwrapperwillsimplyqueueallthereceivedrequests.Then,whenwearenotifiedthattheinitializationiscomplete,weexecuteallthequeuedoperationsandthenswitchtheinternalstatetoinitializedState.Let'sthenseewhatthislastpieceofthewrapperlookslike:
varinitializedState=asyncModule;
Without(probably)anysurprise,theinitializedStateobjectissimplyareferencetotheoriginalasyncModule!Infact,whentheinitializationiscomplete,wecansafelyrouteanyrequestdirectlytotheoriginalmodule,nothingmoreisrequired.
Atlast,wehavetosettheinitialactivestate,whichofcoursewillbenotInitializedState:
varactiveState=notInitializedState;
Wecannowtrytolaunchourtestserveragain,butfirst,let'snotforgettoreplacethereferencestotheoriginalasyncModulemodulewithournewasyncModuleWrapperobject;thishastobedoneintheapp.jsandroutes.jsmodules.
Afterdoingthis,ifwetrytosendarequesttotheserveragain,wewillseethatduringthetime,theasyncModulemoduleisnotyetinitialized;therequestswillnotfail;instead,theywillhanguntiltheinitializationiscompletedandwillonlythenbeactuallyexecuted.Wecansurelyaffirmthatthisisamuchmorerobustbehavior.
TipPattern:Ifamoduleisinitializedasynchronously,queueeveryoperationuntilthemoduleisfullyinitialized.
Now,ourservercanstartacceptingrequestsimmediatelyafterit'sstartedanditguaranteesthatnoneoftheserequestswilleverfailbecauseoftheinitializationstateofitsmodules.WewereabletoobtainthisresultwithoutusingDependencyInjectionorrequiringverboseanderror-pronecheckstoverifythestateoftheasynchronousmodule.
InthewildThepatternwejustpresentedisusedbymanydatabasedriversandORMlibraries.ThemostnotableisMongoose(http://mongoosejs.com),whichisanORMforMongoDB.WithMongoose,it'snotnecessarytowaitforthedatabaseconnectiontoopeninordertobeabletosendqueries,becauseeachoperationisqueuedandthenexecutedlaterwhentheconnectionwiththedatabaseisfullyestablished.ThisclearlybooststheusabilityofitsAPI.
TipTakealookatthecodeofMongoosetoseehoweverymethodinthenativedriverisproxiedtoaddthepreinitializationqueue(italsodemonstratesanalternativewayofimplementingthispattern).Youcanfindthecodefragmentresponsibleforimplementingthepatternathttps://github.com/LearnBoost/mongoose/blob/21f16c62e2f3230fe616745a40f22b4385a11b11/lib/drivers/node-mongodb-native/collection.js#L103-138.
AsynchronousbatchingandcachingInhigh-loadapplications,cachingplaysacriticalroleandisusedalmosteverywhereintheweb,fromstaticresourcessuchaswebpages,images,andstylesheets,topuredatasuchastheresultofdatabasequeries.Inthissection,wearegoingtolearnhowcachingappliestoasynchronousoperationsandhowahighrequestthroughputcanbeturnedtoouradvantage.
ImplementingaserverwithnocachingorbatchingBeforewestartdivingintothisnewchallenge,let'simplementasmalldemoserverthatwewilluseasareferencetomeasuretheimpactofthevarioustechniqueswearegoingtoimplement.
Let'sconsiderawebserverthatmanagesthesalesofane-commercecompany,inparticular,wewanttoqueryourserverforthesumofallthetransactionsofaparticulartypeofmerchandise.Forthispurpose,wearegoingtouseLevelUPagainforitssimplicityandflexibility.Thedatamodelthatwearegoingtouseisasimplelistoftransactionsstoredinthesalessublevel(asectionofthedatabase),whichisorganizedinthefollowingformat:
transactionIdà{amount,item}
ThekeyisrepresentedbytransactionIdandthevalueisaJSONobjectthatcontainstheamountofthesale(amount)andtheitemtype.
Thedatatoprocessisreallybasic,solet'simplementtheAPIimmediatelyinafilenamedtotalSales.js,whichwillbeasfollows:
varlevel=require('level');
varsublevel=require('level-sublevel');
vardb=sublevel(level('example-db',{valueEncoding:
'json'}));
varsalesDb=db.sublevel('sales');
module.exports=functiontotalSales(item,callback){
varsum=0;
salesDb.createValueStream()//[1]
.on('data',function(data){
if(!item||data.item===item){//[2]
sum+=data.amount;
}
})
.on('end',function(){
callback(null,sum);//[3]
});
}
ThecoreofthemoduleisthetotalSalesfunction,whichisalsotheonlyexportedAPI,thisishowitworks:
1. WecreateastreamfromthesalesDbsublevelthatcontainsthesalestransactions.Thestreampullsalltheentriesfromthedatabase.
2. Thedataeventreceiveseachsaletransactionasitisreturnedfromthedatabasestream.Weaddtheamountvalueofthecurrententrytothetotalsumvalue,butonlyiftheitemtypeisequaltotheoneprovidedintheinput(orifnoinputisprovidedatall,allowingustocalculatethesumofallthetransactions,regardlessoftheitemtype).
3. Atlast,whentheendeventisreceived,weinvokethecallback()methodbyprovidingthefinalsumasresult.
Thesimplequerythatwebuiltisdefinitelynotthebestintermsofperformances.Ideally,inareal-worldapplication,wewouldhaveusedanindextoquerythetransactionsbytheitemtype,orevenbetter,anincrementalmap/reducetocalculatethesuminrealtime;however,forourexample,aslowqueryisactuallybetterasitwillhighlighttheadvantagesofthepatternswearegoingtoanalyze.
Tofinalizethetotalsalesapplication,weonlyneedtoexposethetotalSalesAPIfromanHTTPserver;so,thenextstepistobuildone(theapp.jsfile):
varhttp=require('http');
varurl=require('url');
vartotalSales=require('./totalSales');
http.createServer(function(req,res){
varquery=url.parse(req.url,true).query;
totalSales(query.item,function(err,sum){
res.writeHead(200);
res.end('Totalsalesforitem'+
query.item+'is'+sum);
});
}).listen(8000,function(){console.log('Started')});
Theserverwecreatedisveryminimalistic;weonlyneedittoexposethetotalSalesAPI.
Beforewestarttheserverforthefirsttime,weneedtopopulatethedatabasewithsomesampledata;wecandothiswiththepopulate_db.jsscriptthatwecanfindinthecodesamplesdedicatedtothissection.Thescriptwillcreate100Krandomsales
transactionsinthedatabase.
Okay!Now,everythingisreadyinordertostarttheserver;asusualwecandothisbyexecutingthefollowingcommand:
nodeapp
Toquerytheserver,simplynavigatewithabrowsertothefollowingURL:
http://localhost:8000?item=book
However,tohaveabetterideaoftheperformanceofourserver,wewillneedmorethanonerequest;so,wewilluseasmallscriptnamedloadTest.jswhichsendsrequestsatanintervalof200ms.Thescriptcanbefoundinthecodesamplesofthebookandit'salreadyconfiguredtoconnecttotheURLoftheserver,so,torunit,justexecutethefollowingcommand:
nodeloadTest
Wewillseethatthe20requestswilltakeawhiletocomplete,takenoteofthetotalexecutiontimeofthetest,becausewearenowgoingtoapplyouroptimizationsandmeasurehowmuchtimewecansave.
AsynchronousrequestbatchingWhendealingwithasynchronousoperations,themostbasiclevelofcachingcanbeachievedbybatchingtogetherasetofinvocationstothesameAPI.Theideaisverysimple:ifweareinvokinganasynchronousfunctionwhilethereisstillanotheronepending,wecanattachthecallbacktothealreadyrunningoperation,insteadofcreatingabrandnewrequest.Takealookatthefollowingfigure:
Thepreviousimageshowstwoclients(theycanbetwodifferentobjects,ortwodifferentwebrequests)invokingthesameasynchronousoperationwithexactlythesameinput.Ofcourse,thenaturalwaytopicturethissituationiswiththetwoclientsstartingtwoseparateoperationsthatwillcompleteintwodifferentmoments,asshownbytheprecedingimage.Now,considerthenextscenario,depictedinthefollowingfigure:
Thissecondimageshowsushowthetworequests—whichinvokethesameAPIwiththesameinput—canbebatched,orinotherwordsappendedtothesamerunningoperation.Bydoingthis,whentheoperationcompletes,boththeclientswillbenotified.Thisrepresentsasimple,yetextremelypowerful,waytooptimizetheloadofanapplicationwhilenothavingtodealwithmorecomplexcachingmechanisms,whichusuallyrequireanadequatememorymanagementandinvalidationstrategy.
BatchingrequestsinthetotalsaleswebserverLet'snowaddabatchinglayerontopofourtotalSalesAPI.Thepatternwearegoingtouseisverysimple:ifthereisalreadyanotheridenticalrequestpendingwhentheAPIisinvoked,wewilladdthecallbacktoaqueue.Whentheasynchronousoperationcompletes,allthecallbacksinitsqueueareinvokedatonce.
Now,let'sseehowthispatterntranslatesincode.Let'screateanewmodulenamedtotalSalesBatch.js.Here,we'regoingtoimplementabatchinglayerontopoftheoriginaltotalSalesAPI:
vartotalSales=require('./totalSales');
varqueues={};
module.exports=functiontotalSalesBatch(item,callback){
if(queues[item]){//[1]
console.log('Batchingoperation');
returnqueues[item].push(callback);
}
queues[item]=[callback];//[2]
totalSales(item,function(err,res){
varqueue=queues[item];//[3]
queues[item]=null;
queue.forEach(function(cb){
cb(err,res);
});
});
}
ThetotalSalesBatch()functionisaproxyfortheoriginaltotalSales()API,anditworksasfollows:
1. Ifaqueuealreadyexistsfortheitemtypeprovidedastheinput,itmeansthatarequestforthatparticularitemisalreadyrunning.Inthiscase,allwehavetodoissimplyappendthecallbacktotheexistingqueueandreturnfromtheinvocationimmediately.Nothingelseisrequired.
2. Ifnoqueueisdefinedfortheitem,itmeansthatwehavetocreateanewrequest.Todothis,wecreateanewqueueforthatparticularitemandweinitializeitwiththecurrentcallbackfunction.Next,weinvoketheoriginaltotalSales()API.
3. WhentheoriginaltotalSales()requestcompletes,weiterateoverallthecallbacksthatwereaddedinthequeueforthatspecificitemandinvokethemonebyonewiththeresultoftheoperation.
ThebehaviorofthetotalSalesBatch()functionisidenticaltothatoftheoriginaltotalSales()API,withthedifferencethat,now,multiplecallstotheAPIusingthesameinputarebatched,thussavingtimeandresources.
Curioustoknowwhatistheperformanceimprovementcomparedtotheraw,non-batchedversionofthetotalSales()API?Let'sthenreplacethetotalSalesmoduleusedbytheHTTPserverwiththeonewejustcreated(theapp.jsfile):
//vartotalSales=require('./totalSales');
vartotalSales=require('./totalSalesBatch');
http.createServer(function(req,res){
[...]
Ifwenowtrytostarttheserveragainandruntheloadtestagainstit,thefirstthingwewillseeisthattherequestsarereturnedinbatches.Thisistheeffectofthepatternwejustimplementedandit'sagreatpracticaldemonstrationofhowitworks.
Besidesthat,weshouldalsoobserveaconsiderablereductioninthetotaltimeforexecutingthetest;itshouldbeatleastfourtimesfasterthantheoriginaltestperformedagainsttheplaintotalSales()API!
Thisisastunningresult,confirmingthehugeperformanceboostwecanobtainbyjustapplyingasimplebatchinglayer,withoutallthecomplexityofmanagingafull-fledgedcache,andmoreimportantly,withoutworryingaboutinvalidationstrategies.
NoteTherequest-batchingpatternreachesitsbestpotentialinhigh-loadapplicationsandwithslowAPIs,becauseit'sexactlyinthesecircumstancesthatwecanbatchtogetherahighnumberofrequests.
AsynchronousrequestcachingOneoftheproblemswiththerequest-batchingpatternisthatthefastertheAPI,thefewerbatchedrequestsweget.OnecanarguethatifanAPIisalreadyfast,thereisnopointintryingtooptimizeit;however,itstillrepresentsafactorintheresourceloadofanapplicationthat,whensummedup,canstillhaveasubstantialimpact.Also,sometimeswecansafelyassumethattheresultofanAPIinvocationwillnotchangesooften;therefore,asimplerequestbatchingwillnotprovidethebestperformances.Inallthesecircumstances,thebestcandidatetoreducetheloadofanapplicationandincreaseitsresponsivenessisdefinitelyamoreaggressivecachingpattern.
Theideaissimple:assoonasarequestcompletes,westoreitsresultinthecache,whichcanbeavariable,anentryinthedatabase,orinaspecializedcachingserver.Hence,thenexttimetheAPIisinvoked,theresultcanberetrievedimmediatelyfromthecache,insteadofspawninganotherrequest.
Theideaofcachingshouldnotbenewtoanexperienceddeveloper,butwhatmakesthispatterndifferentinasynchronousprogrammingisthatitshouldbecombinedwiththerequestbatching,tobeoptimal.Thereasonisbecausemultiplerequestsmightrunconcurrentlywhilethecacheisnotset,andwhenthoserequestscomplete,thecachewillbesetmultipletimes.
Basedontheseassumptions,thefinalstructureoftheasynchronousrequest-cachingpatternisshowninthefollowingfigure:
Theprecedingfigureshowsusthetwophasesofanoptimalasynchronouscachingalgorithm:
Thefirstphaseistotallyidenticaltothebatchingpattern.Anyrequestreceivedwhilethecacheisnotsetwillbebatchedtogether.Whentherequestcompletes,thecacheisset,once.Whenthecacheisfinallyset,anysubsequentrequestwillbeserveddirectlyfromit.
AnothercrucialdetailtoconsideristheunleashingZalgoanti-pattern(wehaveseenitinactioninChapter1,Node.jsDesignFundamentals).AswearedealingwithasynchronousAPIs,wemustbesuretoalwaysreturnthecachedvalueasynchronously,evenifaccessingthecacheinvolvesonlyasynchronousoperation.
CachingrequestsinthetotalsaleswebserverTodemonstrateandmeasuretheadvantagesoftheasynchronouscachingpattern,let'snowapplywhatwe'velearnedtothetotalSales()API.Asintherequest-batchingexample,wehavetocreateaproxyfortheoriginalAPIwiththesolepurposeofaddingacachinglayer.
Let'sthencreateanewmodulenamedtotalSalesCache.jsthatcontainsthefollowingcode:
vartotalSales=require('./totalSales');
varqueues={};
varcache={};
module.exports=functiontotalSalesBatch(item,callback){
varcached=cache[item];//[1]
if(cached){
console.log('Cachehit');
returnprocess.nextTick(callback.bind(null,null,
cached));
}
if(queues[item]){
console.log('Batchingoperation');
returnqueues[item].push(callback);
}
queues[item]=[callback];
totalSales(item,function(err,res){
if(!err){//[2]
cache[item]=res;
setTimeout(function(){
deletecache[item];
},30*1000);//30secondsexpiry
}
varqueue=queues[item];
queues[item]=null;
queue.forEach(function(cb){
cb(err,res);
});
});
}
Weshouldstraightawayseethattheprecedingcodeisinmanypartsidenticaltowhatweusedfortheasynchronousbatching.Infact,theonlydifferencesarethefollowingones:
1. ThefirstthingthatweneedtodowhentheAPIisinvokedistocheckwhetherthecacheissetandifthat'sthecase,wewillimmediatelyreturnthecachedvalueusingcallback(),makingsuretodeferitwithprocess.nextTick().
2. Theexecutioncontinuesinbatchingmode,butthistime,whentheoriginalAPIsuccessfullycompletes,wesavetheresultintothecache.Wealsosetatimeouttoinvalidatethecacheafter30seconds.Asimplebuteffectivetechnique!
Now,wearereadytotrythetotalSaleswrapperwejustcreated;todothat,weonlyneedtoupdatetheapp.jsmoduleasfollows:
//vartotalSales=require('./totalSales');
//vartotalSales=require('./totalSalesBatch');
vartotalSales=require('./totalSalesCache');
http.createServer(function(req,res){
[...]
Now,theservercanbestartedagainandprofiledusingtheloadTest.jsscriptaswedidinthepreviousexamples.Withthedefaulttestparameters,weshouldseea10-percentreductionintheexecutiontimeascomparedtosimplebatching.Ofcourse,thisishighlydependentonalotoffactors;forexample,thenumberofrequestsreceived,andthedelaybetweenonerequestandtheother.Theadvantagesofusingcachingoverbatchingwillbemuchmoresubstantialwhentheamountofrequestsishigherandspansalongerperiodoftime.
NoteMemoizationisthepracticeofcachingtheresultofafunctioninvocation.Innpm,youcanfindmanypackagestoimplementasynchronousmemoizationwithlittleeffort;oneofthemostcompletepackagesismemoizee(https://npmjs.org/package/memoizee).
NotesaboutimplementingcachingmechanismsWemustrememberthatinreal-lifeapplications,wemightwanttousemoreadvancedinvalidationtechniquesandstoragemechanisms.Thismightbenecessaryforthefollowingreasons:
Alargeamountofcachedvaluesmighteasilyconsumealotofmemory.Inthiscase,aLeastRecentlyUsed(LRU)algorithmcanbeappliedtomaintainconstantmemoryutilization.Whentheapplicationisdistributedacrossmultipleprocesses,usingasimplevariableforthecachemightresultindifferentresultstobereturnedbyeachserverinstance.Ifthat'sundesiredfortheparticularapplicationweareimplementing,thesolutionistouseasharedstoreforthecache.PopularsolutionsareRedis(http://redis.io)andMemcached(http://memcached.org).Amanualcacheinvalidation,asopposedtoatimedexpiry,canenablealonger-livingcacheandatthesametimeprovidemoreup-to-datedata,but,ofcourse,itwouldbealotmorecomplextomanage.
BatchingandcachingwithPromisesInChapter2,AsynchronousControlFlowPatterns,wesawhowPromisescangreatlysimplifyourasynchronouscode,buttheyofferanevenmoreinterestingapplicationwhendealingwithbatchingandcaching.IfwerecallwhatwesaidaboutPromises,therearetwopropertiesthatcanbeexploitedtoouradvantageinthis
circumstance:
Multiplethen()listenerscanbeattachedtothesamepromise.Thethen()listenerisguaranteedtobeinvokedatmostonceanditworksevenifit'sattachedafterthepromiseisalreadyresolved.Moreover,then()isguaranteedtobeinvokedasynchronously,always.
Inshort,thefirstpropertyisexactlywhatweneedforbatchingrequests,whilethesecondmeansthatapromiseisalreadyacachefortheresolvedvalueandoffersanaturalmechanismforreturningacachedvalueinaconsistentasynchronousway.Inotherwords,thismeansthatbatchingandcachingareextremelysimpleandconcisewithPromises.
Todemonstratethis,wecantrytocreateawrapperforthetotalSales()API,usingPromises,andseewhatittakestoaddabatchingandcachinglayer.Let'sseethenhowthislookslike.Let'screateanewmodulenamedtotalSalesPromises.js:
vartotalSales=require('./totalSales');
varPromise=require('bluebird');//[1]
totalSales=Promise.promisify(totalSales);
varcache={};
module.exports=functiontotalSalesPromises(item){
if(cache[item]){//[2]
returncache[item];
}
cache[item]=totalSales(item)//[3]
.then(function(res){//[4]
setTimeout(function(){
deletecache[item];
},30*1000);//30secondsexpiry
returnres;
})
.catch(function(err){//[5]
deletecache[item];
throwerr;
});
returncache[item];//[6]
}
Thefirstthingthatstrikesusisthesimplicityandeleganceofthesolutionweimplementedintheprecedingcode.Promisesareindeedagreattool,butforthisparticularapplicationtheyofferahuge,out-of-the-boxadvantage.Thisiswhathappensintheprecedingcode:
1. First,werequireourPromiseimplementation(bluebird)andthenapplya
promisificationtotheoriginaltotalSales()function.Afterdoingthis,totalSales()willreturnaPromiseinsteadofacceptingacallback.
2. WhenthetotalSalesPromises()wrapperisinvoked,wecheckwhetheracachedPromisealreadyexistsforthegivenitemtype.IfwealreadyhavesuchaPromise,wereturnitbacktothecaller.
3. Ifwedon'thaveaPromiseinthecacheforthegivenitemtype,weproceedtocreateonebyinvokingtheoriginal(promisified)totalSales()API.
4. WhenthePromiseresolves,wesetupatimetoclearthecacheafter30secondsandwereturnrestopropagatetheresultoftheoperationtoanyotherthen()listenerattachedtothePromise.
5. IfthePromiserejectswithanerror,weimmediatelyresetthecacheandthrowtheerroragaintopropagateittothepromisechain,soanyotherlistenerattachedtothesamePromisewillreceivetheerroraswell.
6. Atlast,wereturnthecachedpromisewejustcreated.
Verysimpleandintuitive,andmoreimportantly,wewereabletoachievebothbatchingandcaching.
IfwenowwanttotrythetotalSalesPromise()function,wewillhavetoslightlyadapttheapp.jsmoduleaswell,becausenow,theAPIisusingPromisesinsteadofcallbacks.Let'sdoitbycreatingamodifiedversionoftheappmodulenamedappPromises.js:
varhttp=require('http');
varurl=require('url');
vartotalSales=require('./totalSalesPromises');
http.createServer(function(req,res){
varquery=url.parse(req.url,true).query;
totalSales(query.item).then(function(sum){
res.writeHead(200);
res.end('Totalsalesforitem'+
query.item+'is'+sum);
});
}).listen(8000,function(){console.log('Started')});
ItsimplementationisalmostidenticaltotheoriginalappmodulewiththedifferencethatnowweusethePromise-basedversionofthebatching/cachingwrapper;therefore,thewayweinvokeitisalsoslightlydifferent.
That'sit!Wearenowreadytotrythisnewversionoftheserverbyrunningthefollowingcommand:
nodeappPromises
UsingtheloadTestscript,wecanverifythatthenewimplementationisworkingasexpected.TheexecutiontimeshouldbethesameaswhenwetestedtheserverusingthetotalSalesCache()API.
RunningCPU-boundtasksThetotalSales()API,eventhoughexpensiveintermsofresources,wasnotaffectingtheabilityoftheservertoacceptconcurrentrequests.WhatwelearnedinChapter1,Node.jsDesignFundamentals,abouttheeventloopshouldprovideanexplanationforthisbehavior:invokinganasynchronousoperationcausesthestacktounwindbacktotheeventloop,leavingitfreetohandleotherrequests.
However,whathappenswhenwerunalong,synchronoustaskthatnevergivesbackthecontroltotheeventloop?ThiskindoftaskisalsoknownasCPU-bound,becauseitsmaincharacteristicisthatitisheavyonCPUutilizationratherthanbeingheavyonI/Ooperations.
Let'sworkimmediatelyonanexampletoseehowthesetypesoftasksbehaveinNode.js.
SolvingthesubsetsumproblemLet'snowchooseacomputationallyexpensiveproblemtouseasabaseforourexperiment.Agoodcandidateisthesubsetsumproblemthatconsistsofdecidingwhetheraset(ormultiset)ofintegerscontainsanon-emptysubsetthathasasumequaltozero.Forexample,ifwehadasinputtheset[1,2,-4,5,-3],thesubsetssatisfyingtheproblemare[1,2,-3]and[2,-4,5,-3].
Themostsimplealgorithmistheonethatcheckseverypossiblecombinationofsubsetsofanysize,andithasacomputationalcostofO(2n),orinotherwords,itgrowsexponentiallywiththesizeoftheinput.Thismeansthatasetof20integerswouldrequireupto1,048,576combinationstobechecked,notbadfortestingourassumptions.Ofcourse,thesolutionmightbefoundalotsoonerthanthat;so,tomakethingsharder,wearegoingtoconsiderthefollowingvariationofthesubsetsumproblem:givenasetofintegers,wewanttocalculateallthepossiblecombinationswhosesumisequaltoagivenarbitraryinteger.
Let'sthenworktobuildsuchanalgorithm,let'screateanewmodulecalled'subsetSum.js';wewillstartbycreatingaclasscalledSubsetSum:
varinherits=require('util').inherits;
varEventEmitter=require('events').EventEmitter;
functionSubsetSum(sum,set){
EventEmitter.call(this);
this.sum=sum;
this.set=set;
this.totalSubsets=0;
}
inherits(SubsetSum,EventEmitter);
module.exports=SubsetSum;
TheSubsetSumclassisinheritingfromtheEventEmitter,thisallowsustoproduceaneventeverytimewefindanewsubsetmatchingthesumreceivedasinput.Aswewillsee,thiswillgiveusalotofflexibility.
Next,let'sseehowwecangenerateallthepossiblecombinationsofsubsets:
SubsetSum.prototype._combine=function(set,subset){
for(vari=0;i<set.length;i++){
varnewSubset=subset.concat(set[i]);
this._combine(set.slice(i+1),newSubset);
this._processSubset(newSubset);
}
}
Wewillnotgointotoomuchdetailaboutthealgorithm,buttherearetwoimportantthingstonotice:
The_combine()methodiscompletelysynchronous;itrecursivelygenerateseverypossiblesubsetwithoutevergivingbackthecontroltotheeventloop.Ifwethinkaboutit,thisisperfectlynormalforanalgorithmnotrequiringanyI/O.Everytimeanewcombinationisgenerated,weprovideittothe_processSubset()methodforfurtherprocessing.
The_processSubset()methodisresponsibleforverifyingthatthesumoftheelementsofthegivensubsetisequaltothenumberwearelookingfor:
SubsetSum.prototype._processSubset=function(subset){
console.log('Subset',++this.totalSubsets,subset);
varres=subset.reduce(function(prev,item){
returnprev+item;
},0);
if(res==this.sum){
this.emit('match',subset);
}
}
Trivially,the_processSubset()methodappliesareduceoperationtothesubsetinordertocalculatethesumofitselements.Then,itemitsaneventoftype'match'whentheresultingsumisequaltotheoneweareinterestedinfinding(this.sum).
Finally,thestart()methodputsalltheprecedingpiecestogether:
SubsetSum.prototype.start=function(){
this._combine(this.set,[]);
this.emit('end');
}
Theprecedingmethodtriggersthegenerationofallthecombinationsbyinvoking_combine(),andlastly,emitsan'end'eventsignalingthatallthecombinationswerecheckedandanypossiblematchalreadyemitted.Thisispossiblebecause_combine()issynchronous;therefore,the'end'eventwillbeemittedassoonasthefunctionreturns,whichmeansthatallthecombinationswerecalculated.
Next,wehavetoexposeoverthenetworkthealgorithmwejustcreated,asalwayswecanuseasimpleHTTPserverforthetask.Inparticular,wewanttocreateanendpointintheformat,'/subsetSum?data=<Array>&sum=<Integer>'thatinvokestheSubsetSumalgorithmwiththegivenarrayofintegersandsumtomatch.
Let'sthenimplementthissimpleserverinamodulenamedapp.js:
varhttp=require('http');
varSubsetSum=require('./subsetSum');
http.createServer(function(req,res){
varurl=require('url').parse(req.url,true);
if(url.pathname==='/subsetSum'){
vardata=JSON.parse(url.query.data);
res.writeHead(200);
varsubsetSum=newSubsetSum(url.query.sum,data);
subsetSum.on('match',function(match){
res.write('Match:'+JSON.stringify(match)+'\n');
});
subsetSum.on('end',function(){
res.end();
});
subsetSum.start();
}else{
res.writeHead(200);
res.end('I\malive!\n');
}
}).listen(8000,function(){console.log('Started')});
ThankstothefactthattheSubsetSumobjectreturnsitsresultsusingevents,wecanstreamthematchingsubsetsassoonastheyaregeneratedbythealgorithm,inrealtime.AnotherdetailtomentionisthatourserverrespondswiththetextI'mAlive!everytimewehitaURLdifferentfrom/subsetSum.Wewillusethisforcheckingtheresponsivenessofourserver,aswewillseeinamoment.
Wearenowreadytotryoursubsetsumalgorithm,curioustoknowhowourserver
willhandleit?Let'sthenfireitup:
nodeapp
Assoonastheserverstarts,wearereadytosendourfirstrequest,let'strythenwithasetof17randomnumbers,whichwillresultinthegenerationof131,071combinations,aniceamounttokeepourserverbusyforawhile:
curl-Ghttp://localhost:8000/subsetSum--data-urlencode
"data=[116,
119,101,101,-116,109,101,-105,-102,117,-115,-97,119,-116,-104,
-105,115]"--data-urlencode"sum=0"
Wewillstarttoseetheresultsstreaminglivefromtheserver,butifwetrythefollowingcommandinanotherterminalwhilethefirstrequestisstillrunning,wewillspotahugeproblem:
curl-Ghttp://localhost:8000
WewillimmediatelyseethatthislastrequesthangsuntiltheSubsetSumalgorithmofthefirstrequesthasfinished;theserverisunresponsive!Thiswaskindofwhatweexpected,theNode.jseventlooprunsinasinglethread,andifthisthreadisblockedbyalongsynchronouscomputation,itwillbeunabletoexecuteevenonemorecycleinordertorespondwithasimpleI'malive!.
Wequicklyunderstandthatthisbehaviordoesnotworkforanykindofapplicationmeanttoservemultiplerequests.Butdon'tdespair,inNode.js,wecantacklethistypeofsituationinseveralways,let'sanalyzethetwomostimportantones.
InterleavingwithsetImmediateUsually,aCPU-boundalgorithmisbuiltuponasetofsteps,itcanbeasetofrecursiveinvocations,alooporanyvariation/combinationofthose.So,asimplesolutiontoourproblemwouldbetogivebackthecontroltotheeventloopaftereachoneofthesestepscompletes(orafteracertainnumberofthem).Thisway,anypendingI/Ocanstillbeprocessedbytheeventloopinthoseintervalswherethelong-runningalgorithmyieldstheCPU.AsimplewaytoachievethisistoschedulethenextstepofthealgorithmtorunafteranypendingI/Orequests.ThissoundsliketheperfectusecaseforthesetImmediate()function(wealreadyintroducedthisAPIinChapter1,Node.jsDesignFundamentals).
Note
Pattern:Interleavetheexecutionofalong-runningsynchronoustaskwithsetImmediate().
InterleavingthestepsofthesubsetsumalgorithmLet'snowseehowthispatternappliestothesubsetsumalgorithm.AllwehavetodoisslightlymodifythesubsetSum.jsmodule.Forconvenience,wearegoingtocreateanewmodulecalledsubsetSumDefer.js,takingthecodeoftheoriginalsubsetSumclassasastartingpoint.
Thefirstchangewearegoingtomakeistoaddanewmethodcalled_combineInterleaved(),whichisthecoreofthepatternweareimplementing:
SubsetSumDefer.prototype._combineInterleaved=function(set,
subset){
this.runningCombine++;
setImmediate(function(){
this._combine(set,subset);
if(--this.runningCombine===0){
this.emit('end');
}
}.bind(this));
}
Aswecansee,allwehadtodoisdefertheinvocationoftheoriginal(synchronous)_combine()methodwithsetImmediate().However,nowitbecomesmoredifficulttoknowwhenthefunctionhasfinishedgeneratingallthecombinations,becausethealgorithmisnotsynchronousanymore.Tofixthis,wehavetokeeptrackofalltherunninginstancesofthe_combine()methodusingapatternverysimilartotheasynchronousparallelexecutionwehaveseeninChapter2,AsynchronousControlFlowPatterns.Whenalltheinstancesofthe_combine()methodarefinishedrunning,wecanthenemittheendeventnotifyinganylistenerthattheprocesshascompleted.
TofinalizetherefactoringoftheSubsetSumalgorithm,weneedacoupleofmoretweaks.First,weneedtoreplacetherecursivestepinthe_combine()methodwithitsdeferredcounterpart:
SubsetSumDefer.prototype._combine=function(set,subset){
for(vari=0;i<set.length;i++){
varnewSubset=subset.concat(set[i]);
this._combineInterleaved(set.slice(i+1),newSubset);
this._processSubset(newSubset);
}
}
Withtheprecedingchange,wemakesurethateachstepofthealgorithmwillbequeuedintheeventloopusingsetImmediate()andthereforeexecutedafteranypendingI/Orequest,insteadofrunningsynchronously.
Theothersmalltweakisinthestart()method:
SubsetSumDefer.prototype.start=function(){
this.runningCombine=0;
this._combineInterleaved(this.set,[]);
}
Intheprecedingcode,weinitializethenumberofrunninginstancesofthe_combine()methodto0.Wealsoreplacedthecallto_combine()withacallto_combineInterleaved()andremovedtheemissionofthe'end'event,becausenowthisishandledasynchronouslyin_combineInterleaved().
Withthislastchange,oursubsetsumalgorithmshouldnowbeabletorunitsCPU-boundcodeinstepsinterleavedbyintervalswheretheeventloopcanrunandprocessanyotherpendingrequest.
Thelastmissingbitisupdatingtheapp.jsmodulesothatitcanusethenewversionoftheSubsetSumAPI,thisisactuallyatrivialchange:
varhttp=require('http');
//varSubsetSum=require('./subsetSum');
varSubsetSum=require('./subsetSumDefer');
http.createServer(function(req,res){
[...]
Wearenowreadytotrythisnewversionofthesubsetsumserver.Let'sstarttheappmodulebyusingthefollowingcommand:
nodeapp
Then,trytosendarequestagaintocalculateallthesubsetsmatchingagivensum:
curl-Ghttp://localhost:8000/subsetSum--data-urlencode
"data=[116,
119,101,101,-116,109,101,-105,-102,117,-115,-97,119,-116,-104,
-105,115]"--data-urlencode"sum=0"
Whiletherequestisrunning,wemightnowwanttoseewhethertheserverisresponsive:
curl-Ghttp://localhost:8000
Cool!Thesecondrequestnowshouldreturnimmediately,evenwhileaSubsetSumtaskisrunning,confirmingthatourpatternisworkinggreat.
ConsiderationsontheinterleavingpatternAswesaw,runningaCPU-boundtaskwhilepreservingtheresponsivenessofanapplicationisnotthatcomplicated,itjustrequirestheuseofsetImmediate()toschedulethenextstepofanalgorithmafteranypendingI/O.However,thisisnotthebestpatternintermsofefficiency;infact,deferringataskintroducesasmalloverheadthat,multipliedbyallthestepsthatanalgorithmhastorun,canhaveasignificantimpact.ThisisusuallythelastthingwewantwhenrunningaCPU-boundtask,especiallyifwehavetoreturntheresultdirectlytotheuser,whichshouldhappeninareasonableamountoftime.ApossiblesolutiontomitigatetheproblemwouldbeusingsetImmediate()onlyafteracertainnumberofsteps—insteadofusingitateverysinglestep—butstillthiswouldnotsolvetherootoftheproblem.
Bearinmindthatthisdoesnotmeanthatthepatternwehavejustseenshouldbeavoidedatallcosts,infact,ifwelookatthebiggerpicture,asynchronoustaskdoesnotnecessarilyhavetobeextremelylongandcomplextocreatetroubles.Inabusyserver,evenataskthatblockstheeventloopfor200millisecondscancreateundesirabledelays.Inthosesituations,wherethetaskisexecutedsporadicallyorinthebackgroundanddoesnothavetorunfortoolong,usingsetImmediate()tointerleaveitsexecutionisprobablythesimplestandmosteffectivewaytoavoidblockingtheeventloop.
Noteprocess.nextTick()cannotbeusedtointerleavealong-runningtask.AswesawinChapter1,Node.jsDesignFundamentals,nextTick()schedulesanoperationbeforeanypendingI/O,andthiscaneventuallycauseI/Ostarvationincaseofrepeatedcalls.YoucanverifythatbyyourselfbyreplacingsetImmediate()withprocess.nextTick()intheprevioussample.YoumightalsowanttoknowthatthisbehaviorwasintroducedwithNode.js0.10,infact,withNode.js0.8,process.nextTick()canstillbeusedasaninterleavingmechanism.TakealookatthisGitHubissuetoknowmoreaboutthehistoryandmotivationsofthischangeathttps://github.com/joyent/node/issues/3335
Usingmultipleprocesses
DeferringthestepsofanalgorithmisnottheonlyoptionwehaveforrunningCPU-boundtasks;anotherpatternforpreventingtheeventloopfromblockingisusingchildprocesses.WealreadyknowthatNode.jsgivesitsbestwhenrunningI/Ointensiveapplicationssuchaswebservers,whichallowsustooptimizeresourceutilization,thankstoitsasynchronousarchitecture.
So,thebestwaywehavetomaintaintheresponsivenessofanapplicationistonotrunexpensiveCPU-boundtasksinthecontextofthemainapplicationanduseinsteadseparateprocesses.Thishasthreemainadvantages:
Thesynchronoustaskcanrunatfullspeed,withouttheneedtointerleavethestepsofitsexecutionWorkingwithprocessesinNode.jsissimple,probablyeasierthanmodifyinganalgorithmtousesetImmediate(),andallowsustoeasilyusemultipleprocessorswithouttheneedtoscalethemainapplicationitselfIfwereallyneedmaximumperformances,theexternalprocessmightbecreatedinlower-levellanguages,suchasthegoodoldC(alwaysusethebesttoolforthejob!)
Node.jshasanampletoolbeltofAPIsforinteractingwithexternalprocesses,wecanfindallweneedinthechild_processmodule.Moreover,whentheexternalprocessisjustanotherNode.jsprogram,connectingittothemainapplicationisextremelyeasyandwedon'tevenfeellikewearerunningsomethingexternaltothelocalapplication.Themagichappensthankstothechild_process.fork()function,whichcreatesanewchildNode.jsprocessandalsoautomaticallycreatesacommunicationchannelwithit,allowingustoexchangeinformationusinganinterfaceverysimilartoanEventEmitter.Let'sseehowthisworksbyrefactoringoursubsetsumserveragain.
DelegatingthesubsetsumtasktootherprocessesThegoalfortherefactoringoftheSubsetSumtaskistocreateaseparatechildprocessresponsibleforhandlingthesynchronousprocessing,leavingtheeventloopoftheserverfreetohandlerequestscomingfromthenetwork.Thisistherecipewearegoingtofollowtomakethispossible:
1. WewillcreateanewmodulenamedprocessPool.jsthatwillallowustocreateapoolofrunningprocesses.Startinganewprocessisexpensiveandrequirestime,sokeepingthemconstantlyrunningandreadytohandlerequestsallowsustosavetimeandCPU.Also,thepoolwillhelpuslimitthenumberofprocessesrunningatthesametimetoavoidexposingtheapplicationtoDenialofService(DoS)attacks.
2. Next,wewillcreateamodulecalled'subsetSumFork.js'responsiblefor
abstractingaSubsetSumtaskrunninginachildprocess.Itsrolewillbecommunicatingwiththechildprocessandexposingtheresultsofthetaskasiftheywerecomingfromthecurrentapplication.
3. Atlast,weneedaworker(ourchildprocess),anewNode.jsprogramwiththeonlygoalofrunningtheSubsetSumalgorithmandforwardingitsresultstotheparentprocess.
Implementingaprocesspool
Let'sstartbybuildingtheprocessPool.jsmodulepiecebypiece:
varfork=require('child_process').fork;
functionProcessPool(file,poolMax){
this.file=file;
this.poolMax=poolMax;
this.pool=[];
this.active=[];
this.waiting=[];
}
module.exports=ProcessPool;
Inthefirstpartofthemodule,weimportthechild_process.fork()functionthatwewillusetocreatenewprocesses.Then,wedefinetheProcessPoolconstructorthatacceptsafilerepresentingtheNode.jsprogramtorunandthemaximumnumberofrunninginstancesinthepool(poolMax).Wethendefinethreeinstancevariables:
poolisthesetofrunningprocessesreadytobeusedactivecontainsthelistoftheprocessescurrentlybeingusedwaitingcontainsaqueueofcallbacksforallthoserequeststhatcouldnotbefulfilledimmediatelybecauseofthelackofanavailableprocess
ThenextpieceoftheProcessPoolclassistheacquire()method,whichisresponsibleforreturningaprocessreadytobeused:
ProcessPool.prototype.acquire=function(callback){
varworker;
if(this.pool.length>0){//[1]
worker=this.pool.pop();
this.active.push(worker);
returnprocess.nextTick(callback.bind(null,null,
worker));
}
if(this.active.length>=this.poolMax){//[2]
returnthis.waiting.push(callback);
}
worker=fork(this.file);//[3]
this.active.push(worker);
process.nextTick(callback.bind(null,null,worker));
}
Itslogicisverysimpleandisexplainedasfollows:
1. Ifwehaveaprocessinpoolreadytobeused,wesimplymoveittotheactivelistandthenreturnitbyinvokingcallback(inadeferredfashion,rememberZalgo?).
2. Iftherearenoavailableprocessesinpoolandwealreadyhavereachedthemaximumnumberofrunningprocesses,wehavetowaitforonetobeavailable.Weachievethisbyqueuingthecurrentcallbackinthewaitinglist.
3. Ifwehaven'tyetreachedthemaximumnumberofrunningprocesses,wewillcreateanewoneusingchild_process.fork(),addittotheactivelist,andthenreturnittothecallerusingthecallback.
ThelastmethodoftheProcessPoolclassisrelease(),whosepurposeistoputaprocessbackinthepool:
ProcessPool.prototype.release=function(worker){
if(this.waiting.length>0){//[1]
varwaitingCallback=this.waiting.shift();
waitingCallback(null,worker);
}
this.active=this.active.filter(function(w){//[2]
returnworker!==w;
});
this.pool.push(worker);
}
Theprecedingcodeisalsoverysimpleanditsexplanationisasfollows:
1. Ifthereisarequestinthewaitinglist,wesimplyreassigntheworkerbeingreleasedbypassingittothecallbackattheheadofthewaitingqueue.
2. Otherwise,weremovetheworkerfromtheactivelistandputitbackintopool.
Aswecansee,theprocessesareneverstoppedbutjustreassigned,allowingustosavetimebynotrestartingthemateachrequest.However,it'simportanttoobservethatthismightnotalwaysbethebestchoiceandthisgreatlydependsontherequirementsofourapplication.Possibletweaksforreducinglong-termmemoryusageandaddingrobustnesstoourprocesspoolare:
TerminateidleprocessestofreememoryafteracertaintimeofinactivityAddamechanismtokillnon-responsiveprocesses,orrestartthosethathave
simplycrashed
Butinthisexample,wewillkeeptheimplementationofourprocesspoolsimple,asthedetailswemightwanttoaddarereallyendless.
Communicatingwithachildprocess
NowthatourProcessPoolclassisready,wecanuseittoimplementtheSubsetSumForkwrapperwhoseroleistocommunicatewiththeworkerandexposetheresultsitproduces.Aswesaid,startingaprocesswithchild_process.fork()alsogivesusasimplemessage-basedcommunicationchannel,solet'sseehowthisworksbyimplementingthesubsetSumFork.jsmodule:
varinherits=require('util').inherits;
varEventEmitter=require('events').EventEmitter;
varProcessPool=require('./processPool');
varworkers=newProcessPool(__dirname+
'/subsetSumWorker.js',2);
functionSubsetSumFork(sum,set){
EventEmitter.call(this);
this.sum=sum;
this.set=set;
}
inherits(SubsetSumFork,EventEmitter);
module.exports=SubsetSumFork;
SubsetSumFork.prototype.start=function(){
workers.acquire(function(worker){//[1]
worker.send({sum:this.sum,set:this.set});
varonMessage=function(msg){
if(msg.event==='end'){//[3]
worker.removeListener('message',onMessage);
workers.release(worker);
}
this.emit(msg.event,msg.data);//[4]
}.bind(this);
worker.on('message',onMessage);//[2]
}.bind(this));
}
ThefirstthingtonoticeisthatweinitializedaProcessPoolobjectusingastargetafilenamedsubsetSumWorker.js,whichrepresentsourchildworker.Wealsosettotwothemaximumcapacityofthepool.
AnotherpointworthmentioningisthatwetriedtomaintainthesamepublicAPIofthe
originalSubsetSumclass.Infact,SubsetSumForkisanEventEmitterwhoseconstructoracceptsasumandaset,whilethestart()methodtriggerstheexecutionofthealgorithm,whichrunsonaseparateprocessthistime.Thisiswhathappenswhenthestart()methodisinvoked:
1. Wetrytoacquireanewchildprocessfromthepool.Whenthishappens,weimmediatelyusetheworkerhandletosendthechildprocessamessagewiththeinputofthejobtorun.Thesend()APIisprovidedautomaticallybyNode.jstoallprocessesthatstartwithchild_process.fork(),thisisessentiallythecommunicationchannelthatweweretalkingabout.
2. Wethenstartlisteningforanymessagereturnedfromtheworkerprocess,usingtheon()methodtoattachanewlistener(thisisalsoapartofthecommunicationchannelprovidedbyallprocessesthatstartwithchild_process.fork()).
3. Inthelistener,wefirstcheckwhetherwereceivedanendevent,whichmeansthattheSubsetSumtaskhasfinished,inwhichcaseweremovetheonMessagelistenerandreleasetheworker,puttingitbackintothepool.
4. Theworkerproducesmessagesintheformat{event,data}allowingustoseamlesslyre-emitanyeventproducedbythechildprocess.
That'sitfortheSubsetSumForkwrapper;let'snowimplementtheworkerapplication.
NoteItisgoodtoknowthatthesend()methodavailableonachildprocessinstancecanalsobeusedtopropagateasockethandlefromthemainapplicationtoachildprocess(lookatthedocumentationhttp://nodejs.org/api/child_process.html#child_process_child_send_message_sendhandle).ThisisactuallythetechniqueusedbytheclustermoduletodistributetheloadofanHTTPserveracrossmultipleprocesses(asofNode.js0.10).Wewillseethisinmoredetailinthenextchapter.
Communicatingwiththeparentprocess
Let'snowcreatethesubsetSumWorker.jsmodule,ourworkerapplication,theentirecontentofthismodulewillruninaseparateprocess:
varSubsetSum=require('./subsetSum');
process.on('message',function(msg){//[1]
varsubsetSum=newSubsetSum(msg.sum,msg.set);
subsetSum.on('match',function(data){//[2]
process.send({event:'match',data:data});
});
subsetSum.on('end',function(data){
process.send({event:'end',data:data});
});
subsetSum.start();
});
Wecanimmediatelyseethatwearereusingtheoriginal(andsynchronous)SubsetSumasitis.Nowthatweareinaseparateprocess,wedon'thavetoworrytoblocktheeventloopanymore,alltheHTTPrequestswillcontinuetobehandledbytheeventloopofthemainapplication,withoutdisruptions.
Whentheworkerisstartedasachildprocess,thisiswhathappens:
1. Itimmediatelystartslisteningformessagescomingfromtheparentprocess.Thiscanbeeasilydonewiththeprocess.on()function(also,apartofthecommunicationAPIprovidedwhentheprocessstartsusingchild_process.fork()).TheonlymessageweexpectfromtheparentprocessistheoneprovidingtheinputtoanewSubsetSumtask.Assoonassuchamessageisreceived,wecreateanewinstanceofaSubsetSumclassandregisterthelistenersforthematchandendevents.Lastly,westartthecomputationwithsubsetSum.start().
2. Everytimeaneventisreceivedfromtherunningalgorithm,wewrapitinanobjectwiththeformat,{event,data},andsendittotheparentprocess.ThesemessagesarethenhandledinthesubsetSumFork.jsmodule,aswehaveseenintheprevioussection.
Aswecansee,wejusthadtowrapthealgorithmwealreadybuilt,withoutmodifyingitsinternals.Thisclearlyshowsthatanyportionofanapplicationcanbeeasilyputinanexternalprocessbysimplyusingthepatternwehavejustseen.
TipWhenthechildprocessisnotaNode.jsprogram,thesimplecommunicationchannelwejustdescribedisnotavailable.Inthesesituations,wecanstillestablishaninterfacewiththechildprocessbyimplementingourownprotocolontopofthestandardinputandstandardoutputstreams,whichareexposedtotheparentprocess.
Tofindoutmoreaboutallthecapabilitiesofthechild_processAPI,youcanrefertotheofficialNode.jsdocumentationathttp://nodejs.org/api/child_process.html.
ConsiderationsonthemultiprocesspatternAsalways,totrythisnewversionofthesubsetSumalgorithm,wesimplyhavetoreplacethemoduleusedbytheHTTPserver(fileapp.js):
varhttp=require('http');
//varSubsetSum=require('./subsetSum');
//varSubsetSum=require('./subsetSumDefer');
varSubsetSum=require('./subsetSumFork');
[...]
Wecannowstarttheserveragainandtrytosendasamplerequest:
curl-Ghttp://localhost:8000/subsetSum--data-urlencode
"data=
[116,119,101,101,-116,109,101,-105,-102,117,-115,-97,119,-116,
-104,-105,115]"--data-urlencode"sum=0"
Similartotheinterleavingpatternwehaveseenbefore,alsowiththisnewversionofthesubsetSummoduletheeventloopisnotblockedwhilerunningtheCPU-boundtask.Thiscanbeconfirmedbysendinganotherconcurrentrequestasfollows:
curl-Ghttp://localhost:8000
Theprecedingcommandlineshouldimmediatelyreturnastringasfollows:
I'malive!
Moreinterestingly,wecanalsotrytostarttwosubsetSumtasksconcurrently,wecanseethattheywillusethefullpoweroftwodifferentprocessorsinordertorun(ifoursystemhasmorethanoneprocessor,ofcourse).Instead,ifwetrytorunthreesubsetSumtasksconcurrently,theresultshouldbethatthelastonetostartwillhang.Thisisnotbecausetheeventloopofthemainprocessisblocked,butbecausewesetaconcurrencylimitoftwoprocessesforthesubsetSumtask,whichmeansthatthethirdrequestwillbehandledassoonasatleastoneofthetwoprocessesinthepoolbecomesavailableagain.
Aswesaw,themultiprocesspatternisdefinitelymorepowerfulandflexiblethantheinterleavingpattern;however,it'sstillnotscalable,astheamountofresourcesofferedbyasinglemachineisstillahardlimit.Theanswerinthiscaseistodistributetheloadacrossmultiplemachines,butthisisanotherstoryandfallsunderthecategoryofdistributedarchitecturalpatterns,whichwewillexploreinthenextchapters.
TipItisworthmentioningthatthreadscanbeapossiblealternativetoprocesseswhenrunningCPU-boundtasks.Currently,thereareafewnpmpackagesthatexposeanAPIforworkingwiththreadstouserlandmodules;oneofthemostpopulariswebworker-threads(https://npmjs.org/package/webworker-threads).However,evenifthreadsaremorelightweight,full-fledgedprocessescanoffermoreflexibilityandabetterlevelofisolationinthecaseofproblemssuchasfreezingorcrashing.
SharingcodewiththebrowserOneofthemainsellingpointofNode.jsisthefactthatit'sbasedonJavaScriptandrunsonV8,anenginethatactuallypowersoneofthemostpopularbrowsers:Chrome.Wemightthinkthatthat'senoughtoconcludethatsharingcodebetweenNode.jsandthebrowserisaneasytask;howeveraswewillsee,thisisnotalwaystrue.Unlesswewanttoshareonlysomesmall,self-containedandgenericfragmentsofcode,developingforboththeclientandtheserverrequiresanon-negligiblelevelofeffortinmakingsurethatthesamecodecanrunproperlyintwoenvironmentsthatareintrinsicallydifferent.Forexample,inNode.jswedon'thavetheDOMorlong-livingviews,whileinthebrowserwesurelydon'thavethefilesystemortheabilitytostartnewprocesses.Mostoftheeffortrequiredwhendevelopingforboththeplatformsismakingsuretoreducethosedifferencestotheminimum.Thiscanbedonewiththehelpofabstractionsandpatternsthatenabletheapplicationtoswitch,dynamicallyoratbuildtime,betweenthebrowser-compatiblecodeandtheNode.jscode.
Luckily,withtherisinginterestinthisnewmind-blowingpossibility,manylibrariesandframeworksintheecosystemhavestartedtosupportbothenvironments.Thisevolutionisalsobackedbyagrowingnumberoftoolssupportingthisnewkindofworkflow,whichovertheyearshavebeenrefinedandperfected.ThismeansthatifweareusingannpmpackageonNode.js,thereisagoodprobabilitythatitwillworkseamlesslyonthebrowseraswell.However,thisisoftennotenoughtoguaranteethatourapplicationcanrunwithoutproblemsonboththebrowserandNode.js.Aswewillsee,acarefuldesignisalwaysneededwhendevelopingcross-platformcode.
Inthissection,wearegoingtoexplorethefundamentalproblemswemightencounterwhenwritingcodeforbothNode.jsandthebrowserandwearegoingtoproposesometoolsandpatternsthatcanhelpusintacklingthisnewandexcitingchallenge.
SharingmodulesThefirstwallwehitwhenwewanttosharesomecodebetweenthebrowserandtheserveristhemismatchbetweenthemodulesystemusedbyNode.jsandtheheterogeneouslandscapeofthemodulesystemsusedinthebrowser.Anotherproblemisthatinthebrowserwedon'thavearequire()functionorthefilesystemfromwhichwecanresolvemodules.SoifwewanttowritelargeportionsofcodethatcanworkonboththeplatformsandwewanttocontinuetousetheCommonJSmodulesystem,weneedtotakeanextrastep,weneedatooltohelpusinbundlingallthedependenciestogetheratbuildtimeandabstractingtherequire()mechanismonthebrowser.
UniversalModuleDefinitionInNode.js,weknowperfectlywellthattheCommonJSmodulesarethedefaultmechanismforestablishingdependenciesbetweencomponents.Thesituationinbrowser-spaceisunfortunatelywaymorefragmented:
Wemighthaveanenvironmentwithnomodulesystematall,whichmeansthatglobalsarethemainmechanismtoaccessothermodulesWemighthaveanenvironmentbasedonanAsynchronousModuleDefinition(AMD)loader,asforexample,RequireJS(http://requirejs.org)WemighthaveanenvironmentabstractingtheCommonJSmodulesystem
Luckily,thereisasetofpatternscalledUniversalModuleDefinition(UMD)thatcanhelpusabstractourcodefromthemodulesystemusedintheenvironment.
CreatinganUMDmodule
UMDisnotquitestandardizedyet,sotheremightbemanyvariationsthatdependontheneedsofthecomponentandthemodulesystemsithastosupport.However,thereisoneformthatprobablyisthemostpopularandallowsustosupportthemostcommonmodulesystems,whichareAMD,CommonJS,andbrowserglobals.
Let'sseeasimpleexampleofhowitlookslike.Inanewproject,let'screateanewmodulecalled'umdModule.js':
(function(root,factory){//[1]
if(typeofdefine==='function'&&define.amd){//[2]
define(['mustache'],factory);
}elseif(typeofmodule==='object'&&//[3]
typeofmodule.exports==='object'){
varmustache=require('mustache');
module.exports=factory(mustache);
}else{//[4]
root.UmdModule=factory(root.Mustache);
}
}(this,function(mustache){//[5]
vartemplate='<h1>Hello<i>{{name}}</i></h1>';
mustache.parse(template);
return{
sayHello:function(toWhom){
returnmustache.render(template,{name:toWhom});
}
};
}));
Theprecedingexampledefinesasimplemodulewithoneexternaldependency:
Mustache(http://mustache.github.io),whichisasimpletemplateengine.ThefinalproductoftheprecedingUMDmoduleisanobjectwithonemethodcalledsayHello()thatwillrenderamustachetemplateandreturnittothecaller.ThegoalofUMDisintegratingthemodulewithothermodulesystemsavailableontheenvironment.Thisishowitworks:
1. Allthecodeiswrappedinananonymousself-executingfunction,verysimilartotheRevealingModulepatternwehaveseeninChapter1,Node.jsDesignFundamentals.Thefunctionacceptsarootthatistheglobalnamespaceobjectavailableonthesystem(forexample,windowonthebrowser).Thisisneededmainlyforregisteringthedependencyasaglobalvariable,aswewillseeinamoment.Thesecondargumentisthefactory()ofthemodule,afunctionreturninganinstanceofthemoduleandacceptingitsdependenciesasinput(DependencyInjection).
2. ThefirstthingwedoischeckwhetherAMDisavailableonthesystem.Wedothisbyverifyingtheexistenceofthedefinefunctionanditsamdflag.Iffound,itmeansthatwehaveanAMDloaderonthesystem,soweproceedwithregisteringourmoduleusingdefineandrequiringthedependencymustachetobeinjectedintofactory().
3. WethencheckwhetherweareinaNode.js-flavoredCommonJSenvironmentbycheckingtheexistenceofthemoduleandmodule.exportsobjects.Ifthat'sthecase,weloadthedependenciesofthemoduleusingrequire()andweprovidethemtothefactory().Thereturnvalueofthefactoryisthenassignedtomodule.exports.
4. Lastly,ifwehaveneitherAMDnorCommonJS,weproceedwithassigningthemoduletoaglobalvariable,usingtherootobject,whichinabrowserenvironmentwillusuallybethewindowobject.Also,youcanseehowthedependency,Mustache,isexpectedtobeintheglobalscopeaswell.
5. Asafinalstep,thewrapperfunctionisself-invoked,providingthethisobjectasroot(inthebrowser,itwillbethewindowobject)andprovidingourmodulefactoryasasecondargument.Youcanseehowthefactoryacceptsitsdependenciesasarguments.
TipInthecodedistributedwiththebook,youcanfindasetofexamplesshowinghowtheUMDmodulewejustcreatedcanbeusedincombinationwithanAMDloader,aCommonJSsystem,orsimplywithnoneoftheabove(usingglobals).
ConsiderationsontheUMDpattern
TheUMDpatternisaneffectiveandsimpletechniqueusedforcreatingamodule
compatiblewiththemostpopularmodulesystemsoutthere.However,wehaveseenthatitrequiresalotofboilerplate,whichcanbedifficulttotestineachenvironmentandisinevitablyerror-prone.ThismeansthatmanuallywritingtheUMDboilerplatecanmakesenseforwrappingasinglemoduleandnotasapracticetouseforeverymodulewecreateinourprojects.Itissimplyunfeasibleandimpractical.Inthesesituations,itwouldbebettertoleavethetasktosometoolthatcanhelpusautomatetheprocess,oneofthosetoolsisBrowserify,whichwewillseeinamoment.
Also,weshouldmentionthatAMD,CommonJSandbrowserglobalsarenottheonlymodulesystemsoutthere.Thepatternwehavepresentedwillcovermostoftheusecases,butitrequiresadaptationstosupportanyothermodulesystem.Forexample,theupcomingES6modulespecificationwillbesomethingthatwemightwanttosupportassoonasitgetsstandardized.
TipYoucanfindabroadlistofformalizedUMDpatternsathttps://github.com/umdjs/umd.
IntroducingBrowserifyWhenwritingaNode.jsapplication,thelastthingwewanttodoistomanuallyaddsupportforamodulesystemdifferentfromtheoneoffered,bydefault,bytheplatform.Theidealsituationwouldbecontinuingtowriteourmodulesaswehavealwaysdone,usingrequire()andmodule.exports,andthenuseatooltotransformourcodeintoabundlethatcaneasilyruninthebrowser.Luckily,thisisaproblemthathasalreadybeensolvedbymanyprojects,amongwhichBrowserify(http://browserify.org)isthemostpopularandbroadlysupported.
BrowserifyallowsustowritemodulesusingtheNode.jsmoduleconventionsandthen,thankstoacompilationstep,itcreatesabundle(asingleJavaScriptfile)thatcontainsallthedependenciesourmodulesneedforworking,includinganabstractionoftherequire()function.Thisbundlecanthenbeeasilyincludedintoawebpageandexecutedinsideabrowser.Browserifyrecursivelyscansoursourceslookingforreferencesoftherequire()function,resolving,andthenincludingthereferencedmodulesintothebundle.
TipBrowserifyisnottheonlytoolwehaveforcreatingbrowserbundlesfromNode.js
modules.OtherpopularalternativesareWebmake(https://npmjs.org/package/webmake)andWebpack(https://npmjs.org/package/webpack).Also,require.jsallowsustocreatemodulesforboththeclientandNode.jsbutitusesAMDinplaceofCommonJS(http://requirejs.org/docs/node.html).
ExploringthemagicofBrowserify
Toquicklydemonstratehowthismagicworks,let'sseehowtheumdModulewecreatedintheprevioussectionlookslikeifweuseBrowserify.First,weneedtoinstallBrowserifyitself,wecandoitwithasimplecommand:
npminstallbrowserify-g
The-goptionwilltellnpmtoinstallBrowserifyglobally,sothatwecanaccessitusingasimplecommandfromtheconsole,aswewillseeinamoment.
Next,let'screateafreshprojectandlet'strytobuildamoduleequivalenttotheumdModulewecreatedbefore.ThisishowitlookslikeifwehadtoimplementitinNode.js(filesayHello.js):
varmustache=require('mustache');
vartemplate='<h1>Hello<i>{{name}}</i></h1>';
mustache.parse(template);
module.exports.sayHello=function(toWhom){
returnmustache.render(template,{name:toWhom});
};
DefinitelysimplerthanapplyingaUMDpattern,isn'tit?Now,let'screateafilecalledmain.jsthatistheentrypointofourbrowsercode:
window.addEventListener('load',function(){
varsayHello=require('./sayHello').sayHello;
varhello=sayHello('World!');
varbody=document.getElementsByTagName("body")[0];
body.innerHTML=hello;
});
Intheprecedingcode,werequirethesayHellomoduleinexactlythesamewayaswewoulddoinNode.js,sonomoreannoyancesformanagingdependenciesorconfiguringpaths,asimplerequire()doesthejob.
Next,let'smakesuretohavemustacheinstalledintheproject:
npminstallmustache
Now,comesthemagicalstep.Inaterminal,let'srunthefollowingcommand:
browserifymain.js-obundle.js
Thepreviouscommandwillcompilethemainmoduleandbundlealltherequireddependenciesintoasinglefilecalledbundle.js,whichisnowreadytobeusedinthebrowser!
Toquicklytestthisassumption,let'screateanHTMLpagecalledmagic.htmlthatcontainsthefollowingcode:
<html>
<head>
<title>Browserifymagic</title>
<scriptsrc="bundle.js"></script>
</head>
<body>
</body>
</html>
Thisisenoughforrunningourcodeinthebrowser.Trytoopenthepageandseeitwithyoureyes.Boom!
TipDuringdevelopment,wesurelydon'twanttomanuallyrunbrowserifyateverychangewemaketooursources.Whatwewantinsteadisanautomaticmechanismtoregeneratethebundlewhenoursourceschange.Todothat,wecanusewatchify(https://npmjs.org/package/watchify),acompaniontoolthatwecaninstallbyrunningthefollowingcommand:
npminstallwatchify-g
Watchifycanbeusedintheexactsamewayasbrowserify,thetwotoolshaveasimilarpurposeandcommandlineoptions.Thedifferencebetweenthetwoisthatwatchify,aftercompilingthebundleforthefirsttime,willcontinuetowatchthesourcesoftheprojectsforanychangeandwillthenrebuildthebundleautomaticallybyprocessingonlythechangedfilesformaximumspeed.
TheadvantagesofusingBrowserify
ThemagicofBrowserifydoesn'tstophere.Thisisa(incomplete)listoffeaturesthatmakesharingcodewiththebrowserasimpleandseamlessexperience:
BrowserifyautomaticallyprovidesaversionofthecoreNode.jsmodulesthatarecompatiblewiththebrowser.Thismeansthatwecanusestreams,HTTPclients,Buffers,EventEmitter,andmanymoreinthebrowser!
TipNotethatthefsmoduleisamongthosenotsupported.
Ifwehaveamodulethatisincompatiblewiththebrowser,wecanexcludeitfromthebuild(--excludeoption),replaceitwithanemptyobject(--ignoreoption),orreplaceitwithanothermoduleprovidinganalternativeandbrowser-compatibleimplementation(byusingthe'browser'sectioninthepackage.json).Thisisacrucialfeatureandwewillhavethechancetouseitintheexamplewearegoingtoseeinawhile.BrowserifycangenerateanUMDbundlethatiscompatiblewithothermoduleloaders(--standaloneoption).Browserifyallowsustoperformadditionalprocessingofthesourcefilesusingthird-partytransforms.Thereisatransformforalmosteverythingonemightneed,fromCoffeeScriptcompilation,tosupportforloadingAMD,Bower(http://bower.io),andComponent(http://component.github.io)packagesusingrequire(),fromminificationtothecompilationandbundlingofotherassetssuchastemplatesandstylesheets.
TipYoucanfindalistofalltheavailabletransformsontheproject'swikipageathttps://github.com/substack/node-browserify/wiki/list-of-transforms.
WecaneasilyinvokeBrowserifyfromtaskmanagerssuchasGulp(https://npmjs.org/package/gulp-browserify)andGrunt(https://npmjs.org/package/grunt-browserify).
ThepowerandflexibilityofBrowserifyaresocaptivatingthatmanydevelopersstartedtouseiteventomanageonlyclient-sidecode,inplaceofmorepopularmodulesystemssuchasAMD.Thisisalsomadepossiblebythefactthatmanyclient-sidelibrariesarestartingtosupportCommonJSandnpmbydefault,opening
newandinterestingscenarios.Forexample,wecaninstallJQueryasfollows:
npminstalljquery
Andthenloaditintoourcodewithasimplelineofcode:
var$=require('jquery');
Youwillbesurprisedathowmanyclient-sidelibrariesalreadysupportCommonJSandBrowserify.
TipAgreatresourceforknowingmoreaboutBrowserifyanditscapabilitiesisitsofficialhandbookthatyoucanfindonGitHubathttps://github.com/substack/browserify-handbook.
Fundamentalsofcross-platformdevelopmentWhendevelopingfordifferentplatforms,themostcommonproblemwehavetofaceissharingthecommonpartsofacomponent,whileprovidingdifferentimplementationsforthedetailsthatareplatform-specific.Wewillnowexploresomeoftheprinciplesandthepatternstousewhenfacingthischallenge.
RuntimecodebranchingThemostsimpleandintuitivetechniqueforprovidingdifferentimplementationsbasedonthehostplatformistodynamicallybranchourcode.Thisrequiresthatwehaveamechanismtorecognizeatruntimethehostplatformandthenswitchdynamicallytheimplementationwithanif-elsestatement.IfweareusingBrowserify,thisisassimpleascheckingthevariableprocess.browser,whichisautomaticallysettotruebyBrowserifywhenbundlingourmodules:
if(process.browser){
//clientsidecode
}else{
//Node.jscode
}
SomemoregenericapproachesinvolvecheckingglobalsthatareavailableonlyonNode.jsoronlyinthebrowser.Forexample,wecanchecktheexistenceofthe
windowglobal:
if(window&&window.document){
//clientsidecode
}else{
//Node.jscode
}
UsingaruntimebranchingforswitchingbetweenNode.jsandbrowserimplementationisdefinitelythemostintuitiveandsimplepatternwecanuseforthepurpose;howeverithassomeinconveniences:
Thecodeforboththeplatformsisincludedinthesamemoduleandthereforealsointhefinalbundle,increasingitssizewithunreachablecode.Ifusedtooextensively,itcanconsiderablyreducethereadabilityofthecode,asbusinesslogicwouldbemixedwithlogicmeantonlytoaddcross-platformcompatibility.Usingdynamicbranchingtoloadadifferentmoduledependingontheplatformwillresultinallthemodulestobeaddedtothefinalbundleregardlessoftheirtargetplatform.Forexample,ifweconsiderthenextcodefragment,bothclientModuleandserverModulewillbeincludedinabundlegeneratedwithBrowserify,unlesswedon'texplicitlyexcludeoneofthemfromthebuild:
if(window&&window.document){
require('clientModule');
}else{
require('serverModule');
}
Thislastinconvenienceisduetothefactthatbundlershavenowayofknowingthevalueofaruntimevariableatbuild-time(unlessthevariableisaconstant),sotheyincludeanymoduleregardlessofwhetherit'srequiredfromreachableorunreachablecode.
NoteAconsequenceofthislastpropertyisthatmodulesrequireddynamicallyusingvariablesarenotincludedinthebundle.Forexample,fromthefollowingcode,nomodulewillbebundled:
moduleList.forEach(function(module){
require(module);
});
Build-timecodebranchingMostofthetime,wealreadyknowatbuild-timewhatcodehastobeincludedintheclientbundleandwhatshouldn't.Thismeansthatwecantakethisdecisionupfrontandinstructthebundlertoreplacetheimplementationofamoduleatbuild-time.Thisoftenresultsinaleanerbundle,asweareexcludingunnecessarymodules,andamorereadablecodebecausewedon'thavealltheif-elsestatementsrequiredbyaruntimebranching.
InBrowserify,thismoduleswappingmechanismcanbeeasilyconfiguredinaspecialsectionofthepackage.json.Forexample,considerthefollowingthreemodules:
//moduleA.js
varshowAlert=require('./alert');
//alert.js
module.exports=console.error;
//clientAlert.js
module.exports=alert;
InNode.js,moduleAisusingthedefaultimplementationofthealertmodule,whichwilllogamessagetotheconsole,inthebrowserthoughwewantaproperalertpopuptoshow.Todothat,wecaninstructBrowserifytoswapatbuildtime,theimplementationofthealert.jsmodulewithclientAlert.js.Allweneedtodoistoaddasectionsuchasthefollowingintothepackage.jsonofaproject:
"browser":{
"./alert.js":"./clientAlert.js"
}
Thiswillresultineveryreferencetothealert.jsmodulebeingreplacedwithareferencetotheclientAlert.jsmodule.Thefirstmodulewillnotevenbeincludedinthebundle.
Werealizehowbuild-timebranchingismuchmoreelegantandpowerfulthanruntimebranching.Ononeside,itallowsustocreatemodulesthatarefocusedononlyoneplatform,andontheother,itprovidesasimplemechanismtoexcludeNode.js-onlymodulesfromthefinalbundle.
Designpatternsforcross-platformdevelopmentNowthatweknowhowtoswitchbetweenNode.jsandbrowsercode,theremainingpieceofthepuzzleishowtointegratethiswithinourdesignandhowwecancreate
ourcomponentsinsuchawaythatsomeoftheirpartsareinterchangeable.Thesechallengesshouldnotsoundnewtousatall,infact,allthroughoutthebookwehaveseen,analyzed,andusedpatternstoachievethisverypurpose.
Let'sremindsomeofthemanddescribehowtheyapplytocross-platformdevelopment:
StrategyandTemplate:Thesetwoareprobablythemostusefulpatternswhensharingcodewiththebrowser.Theirintentis,infact,todefinethecommonstepsofanalgorithm,allowingsomeofitspartstobereplaced,whichisexactlywhatweneed!Incross-platformdevelopment,thesepatternsallowustosharetheplatform-agnosticpartofourcomponents,whileallowingtheirplatform-specificpartstobechangedusingadifferentstrategyortemplatemethod(whichcanbechangedusingruntimeorcompile-timebranching).Adapter:Thispatternisprobablythemostusefulwhenweneedtoswapanentirecomponent.InChapter4,DesignPatterns,wehavealreadyseenanexampleofhowanentiremodule,incompatiblewiththebrowser,canbereplacedwithanadapterbuiltontopofabrowser-compatibleinterface.DoyouremembertheLevelUPadapterforthefsinterface?Proxy:Whencodemeanttorunintheserverrunsinthebrowser,weoftenexpectthingsthatliveontheservertobeavailableinthebrowseraswell.ThisiswheretheremoteProxypatterncomesintoplace.Imagineifwewantedtoaccessthefilesystemoftheserverfromthebrowser,wecouldthinkofcreatinganfsobjectontheclientthatproxieseverycalltothefsmodulelivingontheserver,usingAjaxorWebSocketsasawayofexchangingcommandsandreturnvalues.Observer:Theobserverpatternprovidesanaturalabstractionbetweenthecomponentthatemitstheeventandthosethatreceiveit.Incross-platformdevelopment,thismeansthatwecanreplacetheemitterwithitsbrowser-specificimplementationwithoutaffectingthelistenersandviceversa.DependencyInjectionandServicelocator:BothDIandservicelocatorcanbeusefultoreplacetheimplementationofamoduleatthemomentofitsinjection.
Aswecansee,thearsenalofpatternsatourdisposalisquitepowerful,butthemostpowerfulweaponisstilltheabilityofthedevelopertochoosethebestapproachandadaptittothespecificproblemathand.Inthenextsection,wearegoingtoputwhatwelearnedintoaction,leveragingsomeoftheconceptsandpatternswehaveseensofar.
SharingbusinesslogicanddatavalidationusingBackboneModels
Asaperfectconclusionforthissectionandchapter,wearenowgoingtoworkonanapplicationmorecomplexthanusualtodemonstratehowtoperformcodesharingbetweenNode.jsandthebrowser.Wewilltakeasexampleapersonalcontactmanagerapplicationwithverybasicfunctionalities.
Inparticular,weareonlyinterestedinsomebasicCRUDoperationssuchaslisting,creating,andremovingcontacts.Butthemainfeatureofourapplication,theonethatwearereallyinterestedinexploring,isthesharingofthemodelsbetweentheserverandtheclient.Thisisactuallyoneofthemostsoughtaftercapabilitieswhendevelopinganapplicationthathastovalidateandprocessdatabothontheclientandontheserver,whichiswhatmostofthemodernapplicationsactuallyneedtodo.
Togiveyouanidea,thisishowourapplicationshouldlooklikeonceit'scompleted:
Theplanistouseafamiliarstackontheserverwithexpressandlevelup,BackboneViews(http://backbonejs.org)ontheclient,andasetofBackboneModelssharedbetweenNode.jsandthebrowser,toimplementpersistenceandvalidation.Browserifyisourchoiceforbundlingthemodulesforthebrowser.Ifyoudon'tknowBackbone,don'tworry,theconceptswearegoingtodemonstrateherearegeneric
enoughandcanbeunderstoodalsowithoutanyknowledgeofthisframework.
NoteTheprojectwearegoingtoexplorenowisprettylargetobedescribedandwritteninfullonthesepages.Pleasebeadvisedthatonlytherelevantpartswillbeshownhere.Forthefullcode,pleaserefertothesamplesdistributedwiththebook.
ImplementingthesharedmodelsLet'sstartfromthefocalcenterofourapplication,theBackbonemodelswewanttosharewiththebrowser.Inourapplication,wehavetwomodels:Contact,aBackboneModel,andContacts,aBackboneCollection.Let'sseehowtheContactmodulelookslike(themodels/Contact.jsfile):
varBackbone=require('backbone');
varvalidator=require('validator');
module.exports=Backbone.Model.extend({
defaults:{
name:'',
email:'',
phone:''
},
validate:function(attrs,options){
varerrors=[];
if(!validator.isLength(attrs.name,2)){
errors.push('Mustspecifyaname');
}
if(attrs.email&&!validator.isEmail(attrs.email)){
errors.push('Notavalidemail');
}
if(attrs.phone&&!validator.isNumeric(attrs.phone)){
errors.push('Notavalidphone');
}
if(!attrs.phone&&!attrs.email){
errors.push('Mustspecifyatleastonecontact
information');
}
returnerrors.length?errors:undefined;
},
collectionName:'contacts',
url:function(){
if(this.isNew())returnthis.collectionName;
returnthis.collectionName+'/'+this.id;
},
sync:require('./modelSync')
});
Mostoftheprecedingcodeissharedbetweenthebrowserandtheserver,namely,thelogicforsettingthedefaultattributesvaluesandtheirvalidation.Boththedefaults()andvalidate()methodsarepartoftheBackboneframeworkandareoverriddentoprovidethecustomlogicforourmodel.Wealsoaddedanextrafieldtotheobject,calledcollectionName,thatwillbeusedbytheserverforpersistingthemodelintherightsublevel(wewillseethislater)andbytheclientinordertocalculatetheURLoftheRESTAPIendpoint(theurlfield).
Now,comesthebestpart:whenaBackbonemodelissaved,deleted,orfetched(usingsave(),remove(),andfetch()respectively),Backboneinternallydelegatesthetasktothesync()methodofthemodel.Soundsfamiliar?ThisisactuallyaTemplatepatternandit'sperfectforustoperformabuild-timebranchingofourcode.That'sinfactwherethemodelsmusthaveadifferentbehaviorbasedonthetargetenvironment:
Ontheserver,whensave()isinvoked,wewanttopersistamodelinthedatabase;similarly,withfetch(),wewanttoretrievethemodel'sdatafromthedatabase,andwithremove(),wewanttodeleteitOntheclientinstead,wewanteachoneofsave(),fetch(),andremove()totriggeranAJAXcalltotheserver,whichinturnexecutestherequiredoperationandreturnstheresultbacktotheclient
Implementingtheplatform-specificcodeInthecodefragmentgivenearlier,thesyncattributeisafunctionloadedfromthemodelSyncmodule,whichrepresentsourserver-sideimplementationofthemethod.Thisishowitlookslike(themodels/modelSync.jsfile):
vardb=require('../db');
varBackbone=require('backbone');
varuuid=require('node-uuid');
varself=module.exports=function(method,model,options){
switch(method){
case'create':
returnself.saveModel(model,options);
[...]
}
};
self.saveModel=function(model,options){
varcollection=db.sublevel(model.collectionName);
varresults=[];
if(!model.id)model.set('id',uuid.v4());
collection.put(model.id,model.toJSON(),function(err){
if(err)returnoptions.error();
options.success(model.toJSON());
});
}
[...]
WhentheinternalsoftheBackboneModelinvokethesync()method,threeparametersareprovided,asfollows:
Themethodparameterrepresentingtheactionbeingperformed(whichcanbeoneofthefollowing:'create','read','update'or'delete')Themodelparameter,whichistheobjectoftheoperationAsetofoptionsthatcontains,amongotherthings,asuccesscallbacktobeinvokedwhentheoperationcompletesandanerrorcallbacktoinvokeifitfails
Intheprecedingcode,weareshowingwhathappenswhenwereceivea'create'request.Aswecansee,thesaveModel()functionisinvoked,whichsavesthemodelintothedatabase.
Thesync()implementationwehavejustseen,ismeanttobeexecutedonlyontheserver,wherewewanttopersistthedata.Ideally,itcouldalsoworkonthebrowser,becauseLevelUPhasadaptersforIndexedDBandLocalStorage,butthat'snotwhatwewantinthisexample.
Whatwewantinsteadistopersistthedataontheserver,andtodothiswehavetoinvokeawebservicewhenanoperationisperformedonthemodel.ThismeansthatthemodelSyncmoduleisnotgoodforustouseonthebrowser,soweneedadifferentimplementation.Luckily,Backbonealreadyprovidesadefaultimplementationforthesync()methodthatdoesexactlywhatweneed.Sothat'swhatwearegoingtouseontheclient-sideimplementationofthemodelSyncmodule(file:models/clientSync.js):
module.exports=require('backbone').sync;
That'sit,thenextstepistoinstructBrowserifytousethemodulewejustcreatedinplaceofmodelSyncwhencreatingtheclient-sidebundle.Aswehaveseen,thiscanbedoneinthepackage.jsonfile:
[...]
"browser":{
"./models/modelSync.js":"./models/clientSync.js"
[...]
}
Theprecedingfewlinescreateabuild-timebranchingtellingBrowserifytoreplaceanyreferencetothemodule"./models/modelSync.js"withareferenceto"./models/clientSync.js".ThemodulemodelSyncwillnotbeincludedinthefinalbundle.
UsingtheisomorphicmodelsAtthispoint,theContactmoduleshouldbeisomorphic,whichmeansthatitcanruntransparentlybothontheclientandonNode.js.Toshowhowthislookslike,let'sseehowthemodelisusedintheserverroutes(fileroutes.js):
varContact=require('./models/Contact');
[...]
module.exports.createContact=function(req,res,next){
varcontact=newContact(req.body);
contact.once('invalid',function(model,errors){
res.status(400).json({error:errors});
});
contact.save({},{success:function(contact){
res.status(200).json(contact);
}});
}
[...]
ThecreateContact()handlerbuildsanewcontact(Contact)fromtheJSONdatareceivedinthebodyoftherequest(aPOSTtothe'/contacts'URL).Then,weattachtothemodelalistenerfortheinvalidevent,whichtriggerswhenitsattributesdonotpassthevalidationtestswehavedefined.Finally,contact.save()willpersistthedatainthedatabase.
Aswewillsee,thisisexactlywhatwedointhebrowser-sideoftheapplicationaswell.ThishappensintheBackboneViewresponsibleforhandlingthedatasubmittedinaform(fileclient/ContactsView.js):
varBackbone=require('backbone');
varContact=require('../models/Contact');
var$=require('jquery');
[...]
module.exports=Backbone.View.extend({
[...]
createContact:function(evt){
evt.preventDefault();
varcontactJson={
name:$('#newContactForminput[name=name]').val(),
email:$('#newContactForminput[name=email]').val(),
phone:$('#newContactForminput[name=phone]').val()
};
varcontact=newContact(contactJson);//[1]
$('.error-container',this.$el).empty();
contact.once('invalid',this.invalid,this);//[2]
contact.save({},{success:function(){//[3]
this.contacts.add(contact);
}.bind(this)});
}
[...]
});
Aswecansee,whenthecreateContact()functionisinvoked(afterthe"newcontact"formissubmitted),weissuetheexactsamecommandsweusedontheserver:
1. WecreateanewContactmodelfromtheformdata.2. Weregisteralistenerfortheinvalideventsothatwecanimmediatelydisplaya
messagetotheuserifthedatadoesnotpassthevalidation.3. Finally,wesavethemodel,whichthistimewillresultinanHTTPPOSTrequestto
the/contactsURL.
Aswewantedtodemonstrate,ourContactmodelisisomorphicandenablesustoshareitsbusinesslogicandvalidationbetweenthebrowserandtheserver!
RunningtheapplicationTorunthefullsampledistributedwiththebook,don'tforgettoinstallallthemoduleswith:
npminstall
Then,runBrowserifyonthemainmoduleoftheclient-sideapplicationtogeneratethebundleusedonthebrowser:
browserifyclient/main.js-owww/bundle.js
Thenfinally,fireuptheserverwith:
nodeapp
Now,wecanopenourbrowseratthefollowingURLtoaccesstheapplication
http://localhost:8080.
Wecannowverifythatthevalidationisactuallyperformedidenticallyonthebrowserasitisontheserver.Tocheckthisonthebrowser,wecansimplytrytocreateacontactwithaphonenumberthatcontainsletters,whichwillfailthevalidation.Then,totesttheserver-sidevalidation,wecantrytoinvoketheRESTAPIdirectlywithcurl:
curl-XPOSThttp://localhost:8000/contacts–data
'{"name":"John","phone":"wrong"}'--header"Content-
Type:application/json"
Theprecedingcommandshouldreturnanerrorindicatingthatthedatawearetryingtosaveisinvalid.
ThisconcludesourexplorationofthefundamentalprinciplesforsharingcodebetweenNode.jsandthebrowser.Aswehaveseen,thechallengesaremanyandtheefforttodesignisomorphiccodecanbesubstantial.Inthiscontext,it'sworthmentioningthatonebigchallengerelatedtothisareaissharedrendering,whichistheabilitytorenderaviewontheserveraswellasdynamicallyontheclient.Thisrequiresamuchmorecomplexdesigneffortthateasilyaffectstheentirearchitectureoftheapplicationonboththeserverandthebrowser.Manyframeworkstriedtosolvethisultimatechallenge,whichusuallyisthemostcomplexintheareaofcross-platformJavaScriptdevelopment.Amongthoseprojects,wecanfindDerby(http://derbyjs.com),Meteor(https://www.meteor.com),React(http://facebook.github.io/react),andthenRendr(https://github.com/rendrjs/rendr)andEzel(http://ezeljs.com),whicharebasedonBackbone,similartowhatwedidinourexample.
SummaryThischapteraddedsomegreatnewweaponstoourtoolbelt,andaswecannotice,ourjourneyisgettingmorefocusedonspecificproblemsandwehavestartedtodelvedeeplyintomoreadvancedsolutions.Often,wereusedsomeofthepatternswehaveanalyzedinthepreviouschapters:State,Command,andProxytoprovideaneffectiveabstractionforasynchronouslyinitializedmodules,asynchronouscontrolflowpatternstoaddbatchingandcachingtoourAPIs,deferredexecutionandeventstohelpusrunCPU-boundtasks,andfinallyamixofvariousdesignpatternstoenableourmodulestorunseamlesslyinbothNode.jsandthebrowser.
Thischaptergaveusnotonlyasetofrecipestoreuseandcustomizeforourneeds,butalsosomegreatdemonstrationsofhowthemasteringofafewprinciplesandpatternscanhelpustacklethemostcomplexproblemsinNode.jsdevelopment.
Thenexttwochaptersrepresentthepeakofourjourney.Afterstudyingthevarioustactics,wearenowreadytomovetothestrategies,andexplorethepatternsforscalinganddistributingourNode.jsapplications.
Chapter7.ScalabilityandArchitecturalPatternsInitsearlydays,Node.jswasmainlyanon-blockingwebserver;itsoriginalnamewasinfactweb.js.Itscreator,RyanDahl,soonrealizedthepotentialoftheplatformandstartedextendingitwithtoolstoenablethecreationofanytypeofserver-sideapplicationontopoftheduoJavaScript/non-blockingparadigm.ThecharacteristicsofNode.jswereperfectfortheimplementationofdistributedsystems,madeofnodesorchestratingtheiroperationsthroughthenetwork.Node.jswasborntobedistributed.Unlikeotherwebplatforms,thewordscalabilityentersthevocabularyofaNode.jsdeveloperveryearlyinthelifeofanapplication,mainlybecauseofitssingle-threadednature,incapableofexploitingalltheresourcesofamachine,butoftentherearemoreprofoundreasons.Aswewillseeinthischapter,scalinganapplicationdoesnotonlymeanincreasingitscapacity,enablingittohandlemorerequestsfaster;it'salsoacrucialpathtoachievinghighavailabilityandtolerancetoerrors.Amazingly,itcanalsobeawaytosplitthecomplexityofanapplicationintomoremanageablepieces.Scalabilityisaconceptwithmultiplefaces,sixtobeprecise,asmanyasthefacesofacube—thescalecube.
Inthischapter,wewilllearnthefollowingtopics:
WhatthescalecubeisHowtoscalebyrunningmultipleinstancesofthesameapplicationHowtoleveragealoadbalancerwhenscalinganapplicationWhataServiceRegistryisandhowitcanbeusedHowtodesignaMicroservicearchitectureoutofaMonolithicapplicationHowtointegratealargenumberofservicesthroughtheuseofsomesimplearchitecturalpatterns
AnintroductiontoapplicationscalingBeforewediveintosomepracticalpatternsandexamples,itisworthsayingafewwordsaboutthereasonsforscalinganapplicationandhowitcanbeachieved.
ScalingNode.jsapplicationsWealreadyknowthatmostofthetasksofatypicalNode.jsapplicationruninthecontextofasinglethread.InChapter1,Node.jsDesignFundamentals,welearnedthatthisisnotreallyalimitationbutratheranadvantage,becauseitallowsthe
applicationtooptimizetheusageoftheresourcesnecessarytohandleconcurrentrequests,thankstothenon-blockingI/Oparadigm.Asinglethreadfullyexploitedbynon-blockingI/Oworkswonderfullyforapplicationshandlingamoderatenumberofrequestspersecond,usuallyafewhundredpersecond(thisgreatlydependsontheapplication).Assumingweareusingcommodityhardware,thecapacitythatasinglethreadcansupportislimitednomatterhowpowerfulaservercanbe,therefore,ifwewanttouseNode.jsforhigh-loadapplications,theonlywayistoscaleitacrossmultipleprocessesandmachines.
However,workloadisnottheonlyreasontoscaleaNode.jsapplication;infact,withthesametechniques,wecanobtainotherdesirablepropertiessuchasavailabilityandtolerancetofailures.Scalabilityisalsoaconceptapplicabletothesizeandthecomplexityofanapplication;infact,buildingarchitecturesthatcangrowbigisanotherimportantfactorwhendesigningsoftware.JavaScriptisatooltobeusedwithcaution,thelackoftypecheckinganditsmanygotchascanbeanobstacletothegrowthofanapplication,butwithdisciplineandanaccuratedesign,wecanturnthisintoanadvantage.WithJavaScript,weareoftenpushedtokeeptheapplicationsimpleandsplititintomanageablepieces,makingiteasiertoscaleanddistribute.
ThethreedimensionsofscalabilityWhentalkingaboutscalability,thefirstfundamentalprincipletounderstandisloaddistribution,thescienceofsplittingtheloadofanapplicationacrossseveralprocessesandmachines.Therearemanywaystoachievethis,andthebookTheArtofScalabilitybyMartinL.AbbottandMichaelT.Fisher,proposesaningeniousmodeltorepresentthem,calledthescalecube.Thismodeldescribesscalabilityintermsofthefollowingthreedimensions:
XAxis:CloningYAxis:Decomposingbyservice/functionalityZAxis:Splittingbydatapartition
Thesethreedimensionscanberepresentedasacube,asshowninthefollowingfigure:
Thebottom-leftcornerofthecuberepresentstheapplicationshavingalltheirfunctionalitiesandservicesinasinglecodebase(monolithicapplications)andrunningonasingleinstance.Thisisacommonsituationforapplicationshandlingsmallworkloadsorattheearlystagesofdevelopment.
Themostintuitiveevolutionofamonolithic,unscaledapplicationismovingrightalongtheXaxis,whichissimple,mostofthetimeinexpensive(intermsofdevelopmentcost),andhighlyeffective.Theprinciplebehindthistechniqueiselementary—thatis,cloningthesameapplicationntimesandlettingeachinstancehandle1/nthoftheworkload.
ScalingalongtheYaxismeansdecomposingtheapplicationbasedonitsfunctionalities,services,orusecases.Inthisinstance,decomposingmeanscreatingdifferent,standaloneapplications,eachwithitsowncodebase,sometimeswithitsowndedicateddatabase,orevenwithaseparateUI.Forexample,acommonsituationisseparatingthepartofanapplicationresponsiblefortheadministrationfromthepublic-facingproduct.Anotherexampleisextractingtheservicesresponsiblefortheuserauthentication,creatingadedicatedauthenticationserver.Thecriteriatosplitanapplicationbyitsfunctionalitiesdependmostlyonitsbusinessrequirements,theusecases,thedata,andmanyotherfactors,aswewillseelaterinthischapter.Interestingly,thisisthescalingdimensionwiththebiggestrepercussions,notonlyonthearchitectureofanapplication,butalsoonthewayitismanagedfromadevelopmentperspective.Aswewillsee,microservicesisatermthatatthe
momentismostcommonlyassociatedwithafine-grainedYaxisscaling.
ThelastscalingdimensionistheZaxis,wheretheapplicationissplitinsuchawaythateachinstanceisresponsibleforonlyaportionofthewholedata.Thisisatechniquemainlyusedindatabasesandalsotakesthenameofhorizontalpartitioningorsharding.Inthissetup,therearemultipleinstancesofthesameapplication,eachofthemoperatingonapartitionofthedata,whichisdeterminedusingdifferentcriteria.Forexample,wecouldpartitiontheusersofanapplicationbasedontheircountry(listpartitioning),orbasedonthestartingletteroftheirsurname(rangepartitioning),orbylettingahashfunctiondecidethepartitioneachuserbelongsto(hashpartitioning).Eachpartitioncanthenbeassignedtoaparticularinstanceofourapplication.Theuseofdatapartitionsrequireseachoperationtobeprecededbyalookupsteptodeterminewhichinstanceoftheapplicationisresponsibleforagivendatum.Aswesaid,datapartitioningisusuallyappliedandhandledatdatabaselevelbecauseitsmainpurposeisovercomingtheproblemsrelatedtohandlinglargemonolithicdatasets(limiteddiskspace,memory,andnetworkcapacity).Applyingitattheapplicationlevelisworthconsideringonlyforcomplex,distributedarchitecturesorforveryparticularusecasesas,forexample,whenbuildingapplicationsrelyingoncustomsolutionsfordatapersistence,whenusingdatabasesnotsupportingpartitioning,orwhenbuildingapplicationsatGooglescale.Consideringitscomplexity,scalinganapplicationalongtheZaxisshouldbetakeninconsiderationonlyaftertheXandYaxesofthescalecubehavebeenfullyexploited.
Inthenextsections,wewillfocusonthetwomostcommonandeffectivetechniquestoscaleNode.jsapplications,namely,cloninganddecomposingbyfunctionality/service.
CloningandloadbalancingTraditional,multithreadedwebserversareusuallyscaledonlywhentheresourcesassignedtoamachinecannotbeupgradedanymoreorwhendoingsowouldinvolveahighercostthansimplylaunchinganothermachine.Byusingmultiplethreads,traditionalwebserverscantakeadvantageofalltheprocessingpowerofaserver,usingalltheavailableprocessorsandmemory.However,asingleNode.jsprocessisunabletodothat,beingsingle-threadedandhavingamemorylimitof1GB(on64-bitmachines,whichcanbeincreasedtoamaximumof1.7GB).ThismeansthatNode.jsapplicationsareusuallyscaledmuchsoonercomparedtotraditionalwebservers,eveninthecontextofasinglemachine,tobeabletotakeadvantageofallitsresources.
NoteInNode.js,verticalscaling(addingmoreresourcestoasinglemachine)andhorizontalscaling(addingmoremachinestotheinfrastructure)arealmostequivalentconcepts;bothinfactinvolvesimilartechniquestoleveragealltheavailableprocessingpower.
Don'tbefooledintothinkingaboutthisasadisadvantage.Onthecontrary,beingalmostforcedtoscalehasbeneficialeffectsonotherattributesofanapplication,inparticularavailabilityandfault-tolerance.Infact,scalingaNode.jsapplicationbycloningisrelativelysimpleandit'softenimplementedevenifthereisnoneedtoharvestmoreresources,justforthepurposeofhavingaredundant,fail-tolerantsetup.
Thisalsopushesthedevelopertotakeintoaccountscalabilityfromtheearlystagesofanapplication,makingsuretheapplicationdoesnotrelyonanyresourcethatcannotbesharedacrossmultipleprocessesormachines.Infact,anabsoluteprerequisitetoscalinganapplicationisthateachinstancedoesnothavetostorecommoninformationonresourcesthatcannotbeshared,usuallyhardware,suchasmemoryordisk.Forexample,inawebserver,storingthesessiondatainmemoryorondiskisapracticethatdoesnotworkwellwithscaling;instead,usingashareddatabasewillassurethateachinstancewillhaveaccesstothesamesessioninformation,whereveritisdeployed.
Let'snowintroducethemostbasicmechanismforscalingNode.jsapplications:theclustermodule.
TheclustermoduleInNode.js,thesimplestpatterntodistributetheloadofanapplicationacrossdifferentinstancesrunningonasinglemachineisbyusingtheclustermodule,whichispartofthecorelibraries.Theclustermodulesimplifiestheforkingofnewinstancesofthesameapplicationandautomaticallydistributesincomingconnectionsacrossthem,asshowninthefollowingfigure:
Themasterprocessisresponsibleforspawninganumberofprocesses(workers),eachrepresentinganinstanceoftheapplicationwewanttoscale.Eachincomingconnectionisthendistributedacrosstheclonedworkers,spreadingtheloadacrossthem.
NotesonthebehavioroftheclustermoduleInNode.js0.8and0.10,theclustermodulesharesthesameserversocketacrosstheworkersandleavestotheoperatingsystemthejobofload-balancingincomingconnectionsacrosstheavailableworkers.However,thereisaproblemwiththisapproach;infact,thealgorithmsusedbytheoperatingsystemtodistributetheloadacrosstheworkersarenotmeanttoload-balancenetworkrequests,butrathertoscheduletheexecutionofprocesses.Asaresult,thedistributionisnotalwaysuniformacrossalltheinstances;often,afractionofworkersreceivemostoftheload.Thistypeofbehaviorcanmakesensefortheoperatingsystemschedulerbecauseit
focusesonminimizingthecontextswitchesbetweendifferentprocesses.TheshortstoryisthattheclustermoduledoesnotworkatitsfullpotentialinNode.js<=0.10.
However,thesituationchangesstartingfromversion0.11.2,whereanexplicitroundrobinload-balancingalgorithmisincludedinsidethemasterprocess,whichmakessuretherequestsareevenlydistributedacrossalltheworkers.Thenewload-balancingalgorithmisenabledbydefaultonallplatformsexceptWindows,anditcanbegloballymodifiedbysettingthevariablecluster.schedulingPolicy,usingtheconstantscluster.SCHED_RR(roundrobin)orcluster.SCHED_NONE(handledbytheoperatingsystem).
NoteTheroundrobinalgorithmdistributestheloadevenlyacrosstheavailableserversonarotationalbasis.Thefirstrequestisforwardedtothefirstserver,thesecondtothenextserverinthelist,andsoon.Whentheendofthelistisreached,theiterationstartsagainfromthebeginning.Thisisoneofthesimplestandmostusedload-balancingalgorithms;however,it'snottheonlyone.Moresophisticatedalgorithmsallowassigningpriorities,selectingtheleastloadedserverortheonewiththefastestresponsetime.
TipYoucanfindmoredetailsabouttheevolutionoftheclustermoduleinthesetwoNode.jsissues:
https://github.com/joyent/node/issues/3241https://github.com/joyent/node/issues/4435
BuildingasimpleHTTPserverLet'snowstartworkingonanexample.Let'sbuildasmallHTTPserver,clonedandload-balancedusingtheclustermodule.Firstofall,weneedanapplicationtoscale;forthisexamplewedon'tneedtoomuch,justaverybasicHTTPserver.
Let'screateafilecalledapp.jscontainingthefollowingcode:
varhttp=require('http');
varpid=process.pid;
http.createServer(function(req,res){
for(vari=1e7;i>0;i--){}
console.log('Handlingrequestfrom'+pid);
res.end('Hellofrom'+pid+'\n');
}).listen(8080,function(){
console.log('Started'+pid);
});
TheHTTPserverwejustbuiltrespondstoanyrequestbysendingbackamessagecontainingitsPID;thiswillbeusefultoidentifywhichinstanceoftheapplicationishandlingtherequest.Also,tosimulatesomeactualCPUwork,weperformanemptyloop10milliontimes;withoutthis,theserverloadwouldbealmostnothingconsideringthesmallscaleofthetestswearegoingtorunforthisexample.
TipTheappmodulewewanttoscalecanbeanythingandcanalsobeimplementedusingawebframework,forexample,express.
Wecannowcheckifallworksasexpectedbyrunningtheapplicationasusualandsendingarequesttohttp://localhost:8080usingeitherabrowserorcurl.
Wecanalsotrytomeasuretherequestspersecondthattheserverisabletohandleusingonlyoneprocess,forthispurpose,wecanuseanetworkbenchmarkingtoolsuchassiege(http://www.joedog.org/siege-home)orApacheab(http://httpd.apache.org/docs/2.4/programs/ab.html):
siege-c200-t10Shttp://localhost:8080
Withab,thecommandlinewouldbeverysimilar:
ab-c200-t10http://localhost:8080/
Theprecedingcommandswillloadtheserverwith200concurrentconnectionsfor10seconds.Asareference,theresultforasystemwithfourprocessorsisintheorderof90transactionspersecond,withanaverageCPUutilizationofonly20percent.
NotePleaserememberthattheloadtestswewillperforminthischapterareintentionallysimpleandminimalandareprovidedonlyforreferenceandlearning
purposes.Theirresultscannotprovidea100percentaccurateevaluationoftheperformanceofthevarioustechniquesweareanalyzing.
ScalingwiththeclustermoduleLet'snowtrytoscaleourapplicationusingtheclustermodule.Let'screateanewmodulecalledclusteredApp.js:
varcluster=require('cluster');
varos=require('os');
if(cluster.isMaster){
varcpus=os.cpus().length;
//startasmanychildrenasthenumberofCPUs
for(vari=0;i<cpus;i++){//[1]
cluster.fork();
}
}else{
require('./app');//[2]
}
Aswecansee,usingtheclustermodulerequiresverylittleeffort.Let'sanalyzewhatishappening:
1. WhenwelaunchclusteredAppfromthecommandline,weareactuallyexecutingthemasterprocess.Thecluster.isMastervariableissettotrueandtheonlyworkwearerequiredtodoisforkingthecurrentprocessusingcluster.fork().Intheprecedingexample,wearestartingasmanyworkersasthenumberofCPUsinthesystemtotakeadvantageofalltheavailableprocessingpower.
2. Whencluster.fork()isexecutedfromthemasterprocess,thecurrentmainmodule(clusteredApp)isrunagain,butthistimeinworkermode(cluster.isWorkerissettotrue,whilecluster.isMasterisfalse).Whentheapplicationrunsasaworker,itcanstartdoingsomeactualwork.Inourexample,weloadtheappmodule,whichactuallystartsanewHTTPserver.
NoteIt'simportanttorememberthateachworkerisadifferentNode.jsprocesswithitsowneventloop,memoryspace,andloadedmodules.
It'sinterestingtonoticethattheusageoftheclustermoduleisbasedonarecurringpattern,whichmakesitveryeasytorunmultipleinstancesofanapplication:
if(cluster.isMaster){
//fork()
}else{
//dowork
}
TipUnderthehood,theclustermoduleusesthechild_process.fork()API(wealreadymetthisAPIinChapter6,Recipes),therefore,wealsohaveacommunicationchannelavailablebetweenthemasterandtheworkers.Theinstancesoftheworkerscanbeaccessedfromthevariable,cluster.workers,sobroadcastingamessagetoallofthemwouldbeaseasyasrunningthefollowinglinesofcode:
Object.keys(cluster.workers).forEach(function(id){
cluster.workers[id].send('Hellofromthemaster');
});
Now,let'strytorunourHTTPserverinclustermode.WecandothatbystartingtheclusteredAppmoduleasusual:
nodeclusteredApp
Ifourmachinehasmorethanoneprocessor,weshouldseeanumberofworkersbeingstartedbythemasterprocess,oneaftertheother.Forexample,inasystemwithfourprocessors,theterminalshouldlooklikethis:
Started14107
Started14099
Started14102
Started14101
IfwenowtrytohitourserveragainusingtheURLhttp://localhost:8080,weshouldnoticethateachrequestwillreturnamessagewithadifferentPID,whichmeansthattheserequestshavebeenhandledbydifferentworkers,confirmingthattheloadisbeingdistributedamongthem.
Nowwecantrytoloadtestourserveragain:
siege-c200-t10Shttp://localhost:8080
Thisway,weshouldbeabletodiscovertheperformanceincreaseobtainedby
scalingourapplicationacrossmultipleprocesses.Asareference,byusingNode.js0.10inaLinuxsystemwithfourprocessors,theperformanceincreaseshouldbearound3x(270trans/secversus90trans/sec)withanaverageCPUloadof90percent.
ResiliencyandavailabilitywiththeclustermoduleAswealreadymentioned,scalinganapplicationalsobringsotheradvantages,inparticular,theabilitytomaintainacertainlevelofserviceeveninthepresenceofmalfunctionsorcrashes.Thispropertyisalsoknownasresiliencyanditcontributestowardstheavailabilityofasystem.
Bystartingmultipleinstancesofthesameapplication,wearecreatingaredundantsystem,whichmeansthatifoneinstancegoesdownforwhateverreason,westillhaveotherinstancesreadytoserverequests.Thispatternisprettystraightforwardtoimplementusingtheclustermodule.Let'sseehowitworks!
Let'stakethecodefromtheprevioussectionasthestartingpoint.Inparticular,let'smodifytheapp.jsmodulesothatitcrashesafterarandomintervaloftime:
//[...]
//Attheendofapp.js
setTimeout(function(){
thrownewError('Ooops');
},Math.ceil(Math.random()*3)*1000);
Withthischangeinplace,ourserverexitswithanerrorafterarandomnumberofsecondsbetween1and3.Inareal-lifesituation,thiswouldcauseourapplicationtostopworking,andofcourse,serverequests,unlessweusesomeexternaltooltomonitoritsstatusandrestartitautomatically.However,ifweonlyhaveoneinstance,theremaybeanon-negligibledelaybetweenrestartscausedbythestartuptimeoftheapplication.Thismeansthatduringthoserestarts,theapplicationisnotavailable.Havingmultipleinstancesinsteadwillmakesurewealwayshaveabackupsystemtoserveanincomingrequestevenwhenoneoftheworkersfails.
Withtheclustermodule,allwehavetodoisspawnanewworkerassoonaswedetectthatoneisterminatedwithanerrorcode.Let'sthenmodifythe'clusteredApp.js'moduletotakethisintoaccount:
if(cluster.isMaster){
//[...]
cluster.on('exit',function(worker,code){
if(code!=0&&!worker.suicide){
console.log('Workercrashed.Startinganewworker');
cluster.fork();
}
});
}else{
require('./app');
}
Intheprecedingcode,assoonasthemasterprocessreceivesan'exit'event,wecheckwhethertheprocessisterminatedintentionallyorastheresultofanerror;wedothisbycheckingthestatuscodeandtheflagworker.suicide,whichindicateswhethertheworkerwasterminatedexplicitlybythemaster.Ifweconfirmthattheprocesswasterminatedbecauseofanerror,westartanewworker.It'sinterestingtonoticethatwhilethecrashedworkerrestarts,theotherworkerscanstillserverequests,thusnotaffectingtheavailabilityoftheapplication.
Totestthisassumption,wecantrytostressourserveragainusingsiege.Whenthestresstestcompletes,wenoticethatamongthevariousmetricsproducedbysiege,thereisalsoanindicatorthatmeasurestheavailabilityoftheapplication.Theexpectedresultwouldbesomethingsimilartothis:
Transactions:3027hits
Availability:99.31%
[...]
Failedtransactions:21
Pleasebearinmindthatthisresultcanvaryalot;itgreatlydependsonthenumberofrunninginstancesandhowmanytimestheycrashduringthetest,butitshouldgiveagoodindicatorofhowoursolutionworks.Theprecedingnumberstellusthatdespiteourapplicationisconstantlycrashing,weonlyhad21failedrequestsover3,027hits.Intheexamplescenariowebuilt,mostofthefailingrequestswillbecausedbytheinterruptionofalreadyestablishedconnectionsduringacrash.Infact,whenthishappens,siegewillprintanerrorlikethefollowing:
[error]socket:readerrorConnectionresetbypeer
sock.c:479:Connectionresetbypeer
Unfortunately,thereisverylittlewecandotopreventthesetypesoffailures,especiallywhentheapplicationterminatesbecauseofacrash.Nonetheless,oursolutionprovestobeworkinganditsavailabilityisnotbadatallforanapplicationthatcrashessooften!
Zero-downtimerestartANode.jsapplicationmightneedtoberestartedalsowhenitscodeneedstobe
updated.Soalsointhisscenario,havingmultipleinstancescanhelpmaintaintheavailabilityofourapplication.
Whenwehavetointentionallyrestartanapplicationtoupdateit,thereisasmallwindowinwhichtheapplicationrestartsandisunabletoserverequests.Thiscanbeacceptableifweareupdatingourpersonalblog,butit'snotevenanoptionforaprofessionalapplicationwithanSLA(ServiceLevelAgreement)oronethatisupdatedveryoftenaspartofacontinuousdeliveryprocess.Thesolutionistoimplementazero-downtimerestartwherethecodeofanapplicationisupdatedwithoutaffectingitsavailability.
Withtheclustermodule,thisisagainaprettyeasytask,thepatternconsistsinrestartingtheworkersoneatatime.Thisway,theremainingworkerscancontinuetooperateandmaintaintheservicesoftheapplicationavailable.
Let'sthenaddthisnewfeaturetoourclusteredserver;allwehavetodoisaddsomenewcodetobeexecutedbythemasterprocess(theclusteredApp.jsfile):
if(cluster.isMaster){
//[...]
process.on('SIGUSR2',function(){//[1]
console.log('Restartingworkers');
varworkers=Object.keys(cluster.workers);
functionrestartWorker(i){//[2]
if(i>=workers.length)return;
varworker=cluster.workers[workers[i]];
console.log('Stoppingworker:'+worker.process.pid);
worker.disconnect();//[3]
worker.on('exit',function(){
if(!worker.suicide)return;
varnewWorker=cluster.fork();//[4]
newWorker.on('listening',function(){
restartWorker(i+1);//[5]
});
});
}
restartWorker(0);
});
}else{
require('./app');
}
Thisishowtheprecedingblockofcodeworks:
1. TherestartingoftheworkersistriggeredonreceivingtheSIGUSR2signal.2. WedefineaniteratorfunctioncalledrestartWorker().Thisimplementsan
asynchronoussequentialiterationpatternovertheitemsofthecluster.workersobject.
3. ThefirsttaskoftherestartWorker()functionisstoppingaworkergracefullybyinvokingworker.disconnect().
4. Whentheterminatedprocessexits,wecanspawnanewworker.5. Onlywhenthenewworkerisreadyandlisteningfornewconnectionswecan
proceedwithrestartingthenextworkerbyinvokingthenextstepoftheiteration.
NoteAsourprogrammakesuseofUNIXsignals,itwillnotworkproperlyonWindowssystems.Signalsarethesimplestmechanismtoimplementoursolution.However,thisisn'ttheonlyone;infact,otherapproachesincludelisteningforacommandcomingfromasocket,apipe,orthestandardinput.
Nowwecantestourzero-downtimerestartbyrunningtheclusteredAppmoduleandthensendingaSIGUSR2signal.However,firstweneedtoobtainthePIDofthemasterprocess;thefollowingcommandcanbeusefultoidentifyitfromthelistofalltherunningprocesses:
psaf
Themasterprocessshouldbetheparentofasetofnodeprocesses.OncewehavethePIDwearelookingfor,wecansendthesignaltoit:
kill-SIGUSR2<PID>
NowtheoutputoftheclusteredAppapplicationshoulddisplaysomethinglikethis:
Restartingworkers
Stoppingworker:19389
Started19407
Stoppingworker:19390
Started19409
Wecantryagaintousesiegetoverifythatwedon'thaveanyconsiderableimpactontheavailabilityofourapplicationduringtherestartoftheworkers.
Tippm2(https://github.com/Unitech/pm2)isasmallutility,basedoncluster,which
offersloadbalancing,processmonitoring,zero-downtimerestarts,andothergoodies.
DealingwithstatefulcommunicationsTheclustermoduledoesnotworkwellwithstatefulcommunicationswherethestatemaintainedbytheapplicationisnotsharedbetweenthevariousinstances.Thisisbecausedifferentrequestsbelongingtothesamestatefulsessionmaypotentiallybehandledbyadifferentinstanceoftheapplication.Thisisnotaproblemlimitedonlytotheclustermodule,butingeneralitappliestoanykindofstateless,loadbalancingalgorithm.Consider,forexample,thesituationdescribedbythefollowingfigure:
Theuserjohninitiallysendsarequesttoourapplicationtoauthenticatehimself,buttheresultoftheoperationisregisteredlocally(forexample,inmemory),soonlytheinstanceoftheapplicationthatreceivestheauthenticationrequest(InstanceA)knowsthatJohnissuccessfullyauthenticated.WhenJohnsendsanewrequest,theloadbalancermightforwardittoadifferentinstanceoftheapplication,whichactuallydoesn'tpossesstheauthenticationdetailsofjohn,hencerefusingtoperformtheoperation.Theapplicationwejustdescribedcannotbescaledasitis,butluckily,therearetwoeasysolutionswecanapplytosolvetheproblem.
SharingthestateacrossmultipleinstancesThefirstoptionwehavetoscaleanapplicationusingstatefulcommunicationsissharingthestateacrossalltheinstances.Thiscanbeeasilyachievedwithashareddatastore,as,forexample,adatabasesuchasPostgreSQL(http://www.postgresql.org),MongoDB(http://www.mongodb.org),orCouchDB(http://couchdb.apache.org),orevenbetter,wecanuseanin-memorystoresuchasRedis(http://redis.io)orMemcached(http://memcached.org).
Thefollowingdiagramoutlinesthissimpleandeffectivesolution:
Theonlydrawbackofusingasharedstoreforthecommunicationstateisthatit'snotalwayspossible,forexample,wemightbeusinganexistinglibrarythatkeepsthecommunicationstateinmemory;anyway,ifwehaveanexistingapplication,applyingthissolutionrequiresachangeinthecodeoftheapplication(ifit'snotalreadysupported).Aswewillseenext,thereisalessinvasivesolution.
StickyloadbalancingTheotheralternativewehavetosupportstatefulcommunicationsishavingtheloadbalancerroutingalltherequestsassociatedwithasessionalwaystothesameinstanceoftheapplication.Thistechniqueisalsocalledstickyloadbalancing.Thefollowingfigureillustratesasimplifiedscenarioinvolvingthistechnique:
Aswecanseefromtheprecedingfigure,whentheloadbalancerreceivesarequestassociatedtoanewsession,itcreatesamappingwithoneparticularinstanceselectedbytheload-balancingalgorithm.Thenexttimetheloadbalancerreceivesarequestfromthatsamesession,itbypassestheload-balancingalgorithm,selectingtheapplicationinstancethatwaspreviouslyassociatedtothesession.TheparticulartechniquewejustdescribedinvolvestheinspectionofthesessionIDassociatedwiththerequests(usuallyincludedinacookiebytheapplicationortheloadbalanceritself).
AsimpleralternativetoassociateastatefulconnectiontoasingleserverisbyusingtheIPaddressoftheclientperformingtherequest.Usually,theIPisprovidedtoahashfunctionthatgeneratesanIDrepresentingtheapplicationinstancedesignatedtoreceivetherequest.Thistechniquehastheadvantageofnotrequiringtheassociationtoberememberedbytheloadbalancer.Howeveritdoesn'tworkwellwithdeviceschangingtheIPfrequently,as,forexample,whenroamingondifferentnetworks.
Tip
Stickyloadbalancingisnotsupportedbydefaultbytheclustermodule;however,itcanbeaddedwithannpmlibrarycalledsticky-session(https://www.npmjs.org/package/sticky-session).
Onebigproblemwithstickyloadbalancingisthefactthatitnullifiesmostoftheadvantagesofhavingaredundantsystem,wherealltheinstancesoftheapplicationarethesame,andwhereaninstancecaneventuallyreplaceanotheronethatstoppedworking.Forthesereasons,therecommendationistoalwaystrytoavoidstickyloadbalancing,preferringtobuildapplicationsthatmaintainanysessionstateinasharedstoreorthatdon'trequirestatefulcommunicationsatall(forexample,byincludingthestateintherequestitself).
TipForarealexampleofalibraryrequiringastickyloadbalancing,wecanmentionSocket.io(http://socket.io/blog/introducing-socket-io-1-0/#scalability).
ScalingwithareverseproxyTheclustermoduleisnottheonlyoptionwehavetoscaleaNode.jswebapplication.Infact,moretraditionaltechniquesareoftenpreferredbecausetheyoffermorecontrolandpowerinhighlyavailableproductionenvironments.
Thealternativetousingclusteristostartmultiplestandaloneinstancesofthesameapplicationrunningondifferentportsormachines,andthenuseareverseproxy(orgateway)toprovideaccesstothoseinstances,distributingthetrafficacrossthem.Inthisconfiguration,wedon'thaveanymasterprocessdistributingrequeststoasetofworkers,butasetofdistinctprocessesrunningonthesamemachine(usingdifferentports)orscatteredacrossdifferentmachinesinsideanetwork.Toprovideasingleaccesspointtoourapplication,wecanthenuseareverseproxy,aspecialdeviceorserviceplacedbetweentheclientsandtheinstancesofourapplication,whichtakesanyrequestandforwardsittoadestinationserver,returningtheresulttotheclientasifitwasitselftheorigin.Inthisscenario,thereverseproxyisalsousedasaloadbalancer,distributingtherequestsamongtheinstancesoftheapplication.
TipForaclearexplanationofthedifferencesbetweenareverseproxyandaforward
proxy,youcanrefertotheApacheHTTPserverdocumentationathttp://httpd.apache.org/docs/2.4/mod/mod_proxy.html#forwardreverse.
Thenextfigureshowsatypicalmultiprocess,multimachineconfigurationwithareverseproxyactingasaloadbalanceronthefront:
ForaNode.jsapplication,therearemanyreasonstochoosethisapproachinplaceoftheclustermodule:
Areverseproxycandistributetheloadacrossseveralmachines,notjustseveralprocessesThemostpopularreverseproxiesonthemarketsupportstickyloadbalancingAreverseproxycanroutearequesttoanyavailableserver,regardlessofitsprogramminglanguageorplatformWecanchoosemorepowerfulload-balancingalgorithmsManyreverseproxiesalsoofferotherservicessuchasURLrewrites,caching,SSLterminationpoint,oreventhefunctionalityoffull-fledgedwebserversthatcanbeused,forexample,toservestaticfiles
Thatsaid,theclustermodulecouldalsobeeasilycombinedwithareverseproxyifnecessary;forexample,usingclustertoscaleverticallyinsideasinglemachineand
thenusingthereverseproxytoscalehorizontallyacrossdifferentnodes.
NotePattern:useareverseproxytobalancetheloadofanapplicationacrossmultipleinstancesrunningondifferentportsormachines.
Wehavemanyoptionstoimplementaloadbalancerusingareverseproxy;popularsolutionsare:
Nginx(http://nginx.org):Thisisawebserver,reverseproxy,andloadbalancer,builtuponthenon-blockingI/Omodel.HAProxy(http://www.haproxy.org):ThisisafastloadbalancerforTCP/HTTPtraffic.Node.js-basedproxies:TherearemanysolutionsfortheimplementationofreverseproxiesandloadbalancersdirectlyinNode.js.Thismighthaveadvantagesanddisadvantages,aswewillseelater.Cloud-basedproxies:IntheeraofCloudComputing,it'snotraretoutilizealoadbalanceras-a-service.Thiscanbeconvenientbecauseitrequiresminimalmaintenance,it'susuallyhighlyscalable,andsometimes,itcansupportdynamicconfigurationstoenableon-demandscalability.
Inthenextfewsectionsofthischapter,wewillanalyzeasampleconfigurationusingNginxandlateron,wewillalsoworkonbuildingourveryownloadbalancerusingnothingbutNode.js!
LoadbalancingwithNginxTogiveanideaofhowadedicatedreverseproxieswork,wewillnowbuildascalablearchitecturebasedonNginx(http://nginx.org);butfirstweneedtoinstallit.Wecandothatbyfollowingtheinstructionsathttp://nginx.org/en/docs/install.html.
TipOnalatestUbuntusystem,youcanquicklyinstallNginxwiththecommand:
sudoapt-getinstallnginx
OnMacOSX,youcanusebrew(http://brew.sh):
brewinstallnginx
Aswearenotgoingtouseclustertostartmultipleinstancesofourserver,weneedtoslightlymodifythecodeofourapplicationsothatwecanspecifythelisteningportusingacommandlineargument.Thiswillallowustolaunchmultipleinstancesondifferentports.Let'sthenconsideragainthemainmoduleofourexampleapplication(app.js):
varhttp=require('http');
varpid=process.pid;
http.createServer(function(req,res){
for(vari=1e7;i>0;i--){}
console.log('Handlingrequestfrom'+pid);
res.end('Hellofrom'+pid+'\n');
}).listen(process.env.PORT||process.argv[2]||8080,
function(){
console.log('Started'+pid);
});
Thetinychangeishighlightedintheprecedingcode.
Anotherimportantfeaturewelackbynotusingclusteristheautomaticrestartincaseofacrash.Luckilythisiseasytofixbyusingadedicatedsupervisor,whichisanexternalprocessmonitoringourapplicationandrestartingitifnecessary.Possiblechoicesare:
Node.js-basedsupervisorssuchasforever(https://npmjs.org/package/forever)orpm2(https://npmjs.org/package/pm2)OS-basedmonitorssuchasUpstart(http://upstart.ubuntu.com)orSystemd(http://freedesktop.org/wiki/Software/systemd)MoreadvancedmonitoringsolutionssuchasMonit(http://mmonit.com/monit)
Forthisexample,wearegoingtouseforever,whichisthesimplestandmostimmediateforustouse.Wecaninstallitgloballybyrunningthefollowingcommand:
npminstallforever-g
Thenextstepistostartthefourinstancesofourapplication,allondifferentportsandsupervisedbyforever:
foreverstartapp.js8081
foreverstartapp.js8082
foreverstartapp.js8083
foreverstartapp.js8084
Wecancheckthelistofthestartedprocessesusingthecommand:
foreverlist
Nowit'stimetoconfiguretheNginxserverasaloadbalancer.
First,weneedtoidentifythelocationofthenginx.conffilethatcanbefoundinoneofthefollowinglocations,dependingonyoursystem/usr/local/nginx/conf,/etc/nginx,or/usr/local/etc/nginx.
Next,let'sopenthenginx.conffileandapplythefollowingconfiguration,whichistheveryminimumrequiredtogetaworkingloadbalancer:
http{
#[...]
upstreamnodejs_design_patterns_app{
server127.0.0.1:8081;
server127.0.0.1:8082;
server127.0.0.1:8083;
server127.0.0.1:8084;
}
#[...]
server{
listen80;
location/{
proxy_passhttp://nodejs_design_patterns_app;
}
}
#[...]
}
Theconfigurationneedsverylittleexplanation.Intheupstreamnodejs_design_patterns_appsection,wearedefiningalistofthebackendserversusedtohandlethenetworkrequests,andthen,intheserversection,wespecifytheproxy_passdirective,whichessentiallytellsNginxtoforwardanyrequesttotheservergroupwedefinedbefore(nodejs_design_patterns_app).That'sit,nowweonlyneedtoreloadtheNginxconfigurationwiththecommand:
nginx-sreload
Oursystemshouldnowbeup-and-running,readytoacceptrequestsandbalancethetrafficacrossthefourinstancesofourNode.jsapplication.Simplypointyourbrowsertotheaddresshttp://localhosttoseehowthetrafficisbalancedbyourNginxserver.
UsingaServiceRegistryOneimportantadvantageofmoderncloud-basedinfrastructuresistheabilitytodynamicallyadjustthecapacityofanapplicationbasedonthecurrentorpredictedtraffic;thisisalsoknownasdynamicscaling.Ifimplementedproperly,thispracticecanreducethecostoftheITinfrastructureenormouslywhilestillmaintainingtheapplicationhighlyavailableandresponsive.
Theideaissimple;ifourapplicationisexperiencingaperformancedegradationcausedbyapeakinthetraffic,weautomaticallyspawnnewserverstocopewiththeincreasedload.Wecouldalsodecidetoshutdownsomeserversduringcertainhours,as,forexample,atnight,whenweknowthatthetrafficwillbeless,andrestartingthemagaininthemorning.Thismechanismrequirestheloadbalancertoalwaysbeup-to-datewiththecurrentnetworktopology,knowingatanytimewhichserverisup.
AcommonpatterntosolvethisproblemistouseacentralrepositorycalledServiceRegistry,whichkeepstrackoftherunningserversandtheservicestheyprovide.Thenextfigureshowsamultiservicearchitecturewithaloadbalanceronthefront,dynamicallyconfiguredusingaServiceRegistry:
Theprecedingarchitectureassumesthepresenceoftwoservices,APIandWebApp.Theloadbalancerdistributestherequestsarrivingonthe/apiendpointtoalltheserversimplementingtheAPIservice,whiletherestoftherequestsarespreadacrosstheserversimplementingtheWebAppservice.Theloadbalancerobtainsthelistofserversusingtheserviceregistry.
Forthistoworkincompleteautomation,eachapplicationinstancehastoregisteritselftotheserviceregistrythemomentitcomesuponlineandunregisteritselfwhenitstops.Thisway,theloadbalancercanalwayshaveanup-to-dateviewoftheserversandtheservicesavailableinthenetwork.
NotePattern(serviceregistry):useacentralrepositorytostoreanalwaysup-to-dateviewoftheserversandtheservicesavailableinasystem.
Thispatterncanbeappliednotonlytoloadbalancing,butalsomoregenerallyasawaytodecoupleaservicetypefromtheserversprovidingit.WecanlookatitasaServiceLocatordesignpatternappliedtonetworkservices.
Implementingadynamicloadbalancerwithhttp-proxyandseaportTosupportadynamicnetworkinfrastructure,wecanuseareverseproxysuchasNginxorHAProxy;allweneedtodoisupdatetheirconfigurationusinganautomatedserviceandthenforcetheloadbalancertopickthechanges.ForNginx,thiscanbedoneusingthefollowingcommandline:
nginx-sreload
Thesameresultcanbeachievedwithacloud-basedsolution,butwehaveathirdandmorefamiliaralternativethatmakesuseofourfavoriteplatform.
WeallknowthatNode.jsisagreattooltobuildanysortofnetworkapplication;aswesaid,thisisexactlyoneofitsmaindesigngoals.So,whynotbuildaloadbalancerusingnothingbutNode.js?Thiswouldgiveusmuchmorefreedomandpower,andwouldallowustoimplementanysortofpatternoralgorithmstraightintoourcustom-builtloadbalancer,includingtheonewearenowgoingtoexplore,dynamicloadbalancingusingaServiceRegistry.
Forthisexample,wewanttoreplicatethemultiservicearchitecturewesawinthefigureoftheprevioussection,andtodothat,wearegoingtomainlyusetwonpmpackages:
http-proxy(https://npmjs.org/package/http-proxy):ThisisalibrarytosimplifythecreationofproxiesandloadbalancersinNode.jsseaport(https://npmjs.org/package/seaport):ThisisaminimalistServiceRegistrywritteninNode.js
Let'sstartbyimplementingourservices.TheyaresimpleHTTPserversastheoneswehaveusedsofartotestclusterandNginx,butthistimewewanteachservertoregisteritselfintotheServiceRegistrythemomentitstarts.
Let'sseehowthislooks(fileapp.js):
varhttp=require('http');
varpid=process.pid;
varseaport=require('seaport').connect('localhost',9090);
varserviceType=process.argv[2];
varport=seaport.register(serviceType);
http.createServer(function(req,res){
for(vari=1e7;i>0;i--){}
console.log('Handlingrequestfrom'+pid);
res.end(serviceType+'responsefrom'+pid+'\n');
}).listen(port,function(){
console.log('Started'+pid);
});
Intheprecedingcode,therearethreelinesofcodethatdeserveourattention:
1. First,weinitializetheseaportclientandconnectittotheregistryserver,listeningonport9090.
2. Next,wereadfromthecommandlineaserviceType,sowecanstartaserverbychoosingtheserviceitprovides.Thisisonlyforourconvenience,toallowustosimulateamultiservicesetupwithoutimplementingmultipleservers.
3. Finally,weregistertheserviceusingseaport.register().ThisalsoreturnsaportnumberthatweusetobindtheHTTPserver.
TheregistrywillautomaticallyunregistertheservicewhenitlosestheconnectiontotheHTTPserver.Thismeansthatwedon'tneedtomanuallydoit;theserverwillsimplydisappearfromtheregistryassoonasitstops.
Nowit'stimetoimplementtheloadbalancer.Let'sdothatbycreatinganewmodulecalled'loadBalancer.js'.First,weneedtodefinearoutingtabletomapURLpathstoservices:
varrouting=[{
path:'/api',
service:'api-service',
index:0
},{
path:'/',
service:'webapp-service',
index:0
}];
Eachitemintheroutingarraycontainstheserviceusedtohandletherequestsarrivingonthemappedpath.Theindexpropertywillbeusedtoroundrobintherequestsofagivenservice.
Let'sseehowthisworksbyimplementingthesecondpartofloadbalancer.js:
varhttpProxy=require('http-proxy');
varseaport=require('seaport').connect('localhost',9090);
//[1]
varproxy=httpProxy.createProxyServer({});
require('http').createServer(function(req,res){
varroute;
routing.some(function(entry){//[2]
route=entry;
//Startswiththeroutepath?
returnreq.url.indexOf(route.path)===0;
});
varservers=seaport.query(route.service);//[3]
if(!servers.length){
res.writeHead(502);
returnres.end('Badgateway');
}
route.index=(route.index+1)%servers.length;//[4]
proxy.web(req,res,{target:servers[route.index]});
}).listen(8080,function(){console.log('Started');});
ThisishowweimplementedourNode.js-basedloadbalancer:
1. First,weneedtoconnecttotheseaportserversothatwecanhaveaccesstotheregistry.Next,weinstantiateanhttp-proxyobjectandstartanormalwebserver.
2. Intherequesthandleroftheserver,thefirstthingwedoismatchtheURLagainstourroutingtable.Theresultwillbeadescriptorcontainingtheservicename.
3. Weobtainfromseaportthelistofserversimplementingtherequiredservice.Ifthislistisempty,wereturnanerrortotheclient.Formaximumspeed,seaportcachestheregistrylocally,andkeepsitup-to-datebysynchronizingitwiththemainregistryserver.That'swhyseaport.query()isasynchronouscall.
4. Atlast,wecanroutetherequesttoitsdestination.Weupdateroute.indextopointtothenextserverinthelist,followingaroundrobinapproach.Wethenusetheindextoselectaserverfromthelist,passingittoproxy.web()alongwiththerequest(req)andtheresponse(res)objects.Thiswillsimplyforwardtherequesttotheserverwechose.
ItisnowclearhowsimpleitistoimplementaloadbalancerusingonlyNode.jsandhowmuchflexibilitywecanhavebydoingso.Nowweshouldbereadytogiveitago,butfirst,let'sinstalltheseaportserverbyrunningthefollowingcommand:
npminstallseaport-g
Thisallowsustostarttheseaportserviceregistrywiththissimplecommandline:
seaportlisten9090
Nowwearereadytostarttheloadbalancer:
nodeloadBalancer
Nowifwetrytoaccesssomeoftheservicesexposedbytheloadbalancer,wewillnoticethatitreturnsanHTTP502error,becausewedidn'tstartanyserveryet.Tryityourself:
curllocalhost:8080/api
Theprecedingcommandshouldreturnthefollowingoutput:
BadGateway
Thesituationwillchangeifwespawnsomeinstancesofourservices,forexample,twoapi-serviceandonewebapp-service:
foreverstartapp.jsapi-service
foreverstartapp.jsapi-service
foreverstartapp.jswebapp-service
Nowtheloadbalancershouldautomaticallyseethenewserversandstartdistributingrequestsacrossthem.Let'stryagainwiththefollowingcommand:
curllocalhost:8080/api
Theprecedingcommandshouldnowreturnthis:
api-serviceresponsefrom6972
Byrunningitagain,weshouldnowreceiveamessagefromanotherserver,confirmingthattherequestsarebeingdistributedevenlyamongthedifferentservers:
api-serviceresponsefrom6979
Theadvantagesofthispatternareimmediate.Wecannowscaleourinfrastructuredynamically,on-demand,orbasedonaschedule,andourloadbalancerwillautomaticallyadjustwiththenewconfigurationwithoutanyextraeffort!
Peer-to-peerloadbalancing
UsingareverseproxyisalmostanecessitywhenwewanttoexposeacomplexinternalnetworkarchitecturetoapublicnetworksuchastheInternet.Ithelpshidethecomplexity,providingasingleaccesspointthatexternalapplicationscaneasilyuseandrelyon.However,ifweneedtoscaleaservicethatisforinternaluseonly,wecanhavemuchmoreflexibilityandcontrol.
Let'simaginehavingaServiceAwhichreliesonaServiceBtoimplementitsfunctionality.ServiceBisscaledacrossmultiplemachinesandit'savailableonlyintheinternalnetwork.WhatwehavelearnedsofaristhatServiceAwillconnecttoServiceBusingareverseproxy,whichwilldistributethetraffictoalltheserversimplementingServiceB.
However,thereisanalternative.Wecanremovethereverseproxyfromthepictureanddistributetherequestsdirectlyfromtheclient(ServiceA),whichnowbecomesdirectlyresponsibleforloadbalancingitsconnectionsacrossthevariousinstancesofServiceB.ThisispossibleonlyifServerAknowsthedetailsabouttheserversexposingServiceB,andinaninternalnetwork,thisisusuallyknowninformation.Withthisapproachweareessentiallyimplementingpeer-to-peerloadbalancing.
Thefollowingdiagramcomparesthetwoalternativeswejustdescribed:
Thisisanextremelysimpleandeffectivepatternthatenablestrulydistributedcommunicationswithoutbottlenecksorsinglepointsoffailure.Besidesthat,italsodoesthefollowing:
ReducestheinfrastructurecomplexitybyremovinganetworknodeAllowsfastercommunications,becausemessageswilltravelthroughonefewernodeScalesbetter,becauseperformancesarenotlimitedbywhattheloadbalancercanhandle
Ontheotherside,byremovingthereverseproxy,weareactuallyexposingthecomplexityofitsunderlyinginfrastructure.Also,eachclienthastobesmarterbyimplementingaload-balancingalgorithmandpossibly,alsoawaytokeepitsknowledgeoftheinfrastructureup-to-date.
TipPeer-to-peerloadbalancingisapatternusedextensivelyintheØMQ(http://zeromq.org)library.
ImplementinganHTTPclientthatcanbalancerequestsacrossmultipleserversWealreadyknowhowtoimplementaloadbalancerusingonlyNode.jsanddistributeincomingrequestsacrosstheavailableservers,soimplementingthesamemechanismontheclientsideshouldnotbethatdifferent.AllwehavetodoinfactiswraptheclientAPIandaugmentitwithaload-balancingmechanism.Takealookatthefollowingmodule(balancedRequest.js):
varhttp=require('http');
varservers=[
{host:'localhost',port:'8081'},
{host:'localhost',port:'8082'}
];
vari=0;
module.exports=function(options,callback){
i=(i+1)%servers.length;
options.hostname=servers[i].host;
options.port=servers[i].port;
returnhttp.request(options,callback);
};
Theprecedingcodeisverysimpleandneedslittleexplanation.Wewrappedtheoriginalhttp.requestAPIsothatitoverrideshostnameandportoftherequestwiththoseselectedfromthelistofavailableserversusingaroundrobinalgorithm.
ThenewwrappedAPIcanthenbeusedseamlessly(client.js):
varrequest=require('./balancedRequest');
for(vari=10;i>=0;i--){
request({method:'GET',path:'/'},function(res){
varstr='';
res.on('data',function(chunk){
str+=chunk;
}).on('end',function(){
console.log(str);
});
}).end();
}
Totrytheprecedingcode,wehavetostarttwoinstancesofthesampleserverprovided:
nodeapp8081
nodeapp8082
Followedbytheclientapplicationwejustbuilt:
nodeclient
Weshouldnoticehoweachrequestissenttoadifferentserver,confirmingthatwearenowabletobalancetheloadwithoutadedicatedreverseproxy!
TipAnimprovementtothewrapperwecreatedbeforewouldbetointegrateaServiceRegistrydirectlyintotheclientandobtaintheserverlistdynamically.Youcanfindanexampleofthistechniqueinthecodedistributedwiththebook.
DecomposingcomplexapplicationsSofarinthechapter,wehavemainlyfocusedouranalysisontheXaxisofthescalecube.Wesawhowitrepresentstheeasiestandmostimmediatewaytodistributetheloadofanapplication,alsoimprovingitsavailability.Inthefollowingsection,wearenowgoingtofocusontheYaxisofthescalecube,whereapplicationsarescaledbydecomposingthembyfunctionalityandservice.Aswewilllearn,thistechniqueallowstoscalenotonlythecapacityofanapplication,butalso,andmostimportantly,itscomplexity.
MonolithicarchitectureThetermmonolithicmightmakeusthinkofasystemwithoutmodularity,wherealltheservicesofanapplicationareinterconnectedtogetherandalmostindistinguishable.However,thisisnotalwaysthecase.Often,monolithicsystemshaveahighlymodulararchitectureandagooddecouplingbetweentheirinternalcomponents.
AperfectexampleistheLinuxOperatingSystemkernel,whichispartofacategorycalledmonolithickernels(inperfectoppositionwithitsecosystemandtheUnixphilosophy).Linuxhasthousandsofservicesandmodulesthatwecanloadandunloaddynamicallyevenwhilethesystemisrunning.However,theyallruninkernelmode,whichmeansthatafailureinanyofthemmightbringtheentireOSdown(haveyoueverseenakernelpanic?).Thisapproachisopposedtothemicrokernelarchitecture,whereonlythecoreservicesoftheoperatingsystemruninkernelmode,whiletherestisrunninginusermode,usuallyeachonewithitsownprocess.Themainadvantageofthisapproachisthataprobleminanyoftheseserviceswouldmorelikelycauseittocrashinisolationinsteadofaffectingthestabilityoftheentiresystem.
NoteTheTorvalds-Tanenbaumdebateonkerneldesignisprobablyoneofthemostfamousflamewarsinthehistoryofcomputerscience,whereoneofthemainpointsofdisputewasexactlymonolithicversusmicrokerneldesign.Youcanfindawebversionofthediscussion(itoriginallyappearedonUsenet)athttps://groups.google.com/d/msg/comp.os.minix/wlhw16QWltI/P8isWhZ8PJ8J.
It'sremarkablehowthesedesignprinciples,morethan30yearsold,canstillbeappliedtodayandintotallydifferentenvironments.Modernmonolithicapplicationsare
comparabletomonolithickernels;ifanyoftheircomponentsfail,theentiresystemisaffected,whichtranslatedintoNode.jstermsmeansthatalltheservicesarepartofthesamecodebaseandruninasingleprocess(whennotcloned).
Tomakeanexampleofamonolithicarchitecture,let'stakealookatthefollowingfigure:
Theprecedingfigureshowsthearchitectureofatypicale-commerceapplication.Itsstructureismodular;wehavetwodifferentfrontends,oneforthemainstoreandanotherfortheadministrationinterface.Internally,wehaveaclearseparationoftheservicesimplementedbytheapplication,eachoneresponsibleforaspecificportionofitsbusinesslogic:Products,Cart,Checkout,Search,andAuthenticationandUsers.However,theprecedingarchitectureismonolithic,everymodule,infact,ispartofthesamecodebaseandrunsaspartofasingleapplication.Afailureinanyofitscomponents,forexample,anuncaughtexception,canpotentiallyteardowntheentireonlinestore.
Anotherproblemwiththistypeofarchitectureistheinterconnectionbetweenitsmodules;thefactthattheyallliveinsidethesameapplicationmakesitveryeasyforadevelopertobuildinteractionsandcouplingbetweenmodules.Forexample,considertheusecasewhenaproductisbeingpurchased;theCheckoutmodulehastoupdatetheavailabilityoftheProductobject,andifthosetwomodulesareinthesameapplication,it'stooeasyforadevelopertojustobtainareferencetoaProductobjectandupdateitsavailabilitydirectly.Maintainingalowcouplingbetweeninternalmodulesisveryhardinmonolithicapplication,partlybecausetheboundariesbetween
themarenotalwaysclearorproperlyenforced.
Ahighcouplingisoftenoneofthemainobstaclestothegrowthofanapplicationandpreventsitsscalabilityintermsofcomplexity.Infact,anintricatedependencygraphmeansthateverypartofthesystemisaliability;ithastobemaintainedfortheentirelifeoftheproduct,andanychangeshouldbecarefullyevaluatedbecauseeverycomponentislikeawoodenblockinaJengatower,movingorremovingoneofthemcancausetheentiretowertocollapse.Thisoftenresultsinthebuildingofconventionsanddevelopmentprocessestocopewiththeincreasingcomplexityoftheproject.
TheMicroservicearchitectureNowwearegoingtorevealthemostimportantpatterninNode.jstowritebigapplications:avoidwritingbigapplications.Thisseemslikeatrivialstatement,butit'sanincrediblyeffectivestrategytoscaleboththecomplexityandthecapacityofasoftwaresystem.Sowhat'sthealternativetowritingbigapplications?TheanswerisintheYaxisofthescalecube,decompositionandsplittingbyserviceandfunctionality.Theideaistobreakdownanapplicationintoitsessentialcomponents,creatingseparate,independentapplications.Itispracticallytheoppositeofthemonolithicarchitecture.ThisfitsperfectlywiththeUnixphilosophy,andtheNode.jsprincipleswediscussedinthebeginningofthebook,inparticular"makeeachprogramdoonethingwell".
TheMicroservicearchitectureisprobablytodaythereferencepatternforthistypeofapproach,whereasetofself-sufficientservicesreplacesbigmonolithicapplications.Theprefixmicromeansthattheservicesshouldbeassmallaspossible,butalwayswithinreasonablelimits.Don'tbemisledbythinkingthatcreatinganarchitecturewithahundreddifferentapplicationsexposingonlyonewebserviceisnecessarilyagoodchoice.Inreality,thereisnostrictruleonhowsmallorbigaserviceshouldbe,it'snotthesizethatmattersinthedesignofaMicroservicearchitecture;instead,it'sacombinationofdifferentfactors,mainlyloosecoupling,highcohesion,andintegrationcomplexity.
AnexampleoftheMicroservicearchitectureLet'snowseehowthemonolithice-commerceapplicationwouldlooklike,usingaMicroservicearchitecture:
Aswecanseefromthepreviousfigure,eachfundamentalcomponentofthee-commerceapplicationisnowaself-sustainingandindependententity,livinginitsowncontext,withitsowndatabase.Inpractice,theyareallindependentapplicationsexposingasetofrelatedservices(highcohesion).
ThedataownershipofaserviceisanimportantcharacteristicoftheMicroservicearchitecture.Thisiswhythedatabasealsohastobesplittomaintaintheproperlevelofisolationandindependence.Ifauniqueshareddatabaseisused,itwouldbecomemucheasierfortheservicestoworktogether;however,thiswouldalsointroduceacouplingbetweentheservices(basedondata),nullifyingsomeoftheadvantagesofhavingdifferentapplications.
Thedashedlineconnectingallthenodestellsusthat,insomeway,theyhavetocommunicateandexchangeinformationfortheentiresystemtobefullyfunctional.Astheservicesdonotsharethesamedatabase,thereismorecommunicationinvolvedtomaintaintheconsistencyofthewholesystem.Forexample,theCheckoutapplicationneedstoknowsomeinformationaboutProducts,suchasthepriceandrestrictionsonshipping,andatthesametime,itneedstoupdatethedatastoredin
theProductsservice,forexample,theproductavailabilitywhenthecheckoutiscomplete.Intheprecedingfigure,wetriedtokeepthewaythenodescommunicateabstract.Surely,themostpopularstrategyisusingwebservices,butaswewillseelater,thisisnottheonlyoption.
NotePattern(microservicearchitecture):splitacomplexapplicationbycreatingseveralsmall,self-containedservices.
ProsandconsofmicroservicesInthissectionwearegoingtohighlightsomeoftheadvantagesanddisadvantagesofimplementingtheMicroservicearchitecture.Aswewillsee,thisapproachpromisestobringaradicalchangeinthewaywedevelopourapplications,revolutionizingthewayweseescalabilityandcomplexity,butontheotherside,itintroducesnewnontrivialchallengesaswell.
NoteMartinFowlerwroteagreatarticleaboutmicroservicesthatyoucanfindathttp://martinfowler.com/articles/microservices.html.
Everyserviceisexpendable
Themaintechnicaladvantageofhavingeachservicelivinginitsownapplicationcontextisthatcrashes,bugs,andbreakingchangesdonotpropagatetotheentiresystem.Thegoalistobuildtrulyindependentservicesthataresmaller,easiertochange,orevenrebuildfromscratch.If,forexample,theCheckoutserviceofoure-commerceapplicationsuddenlycrashesbecauseofaseriousbug,therestofthesystemwouldcontinuetoworkasnormal.Somefunctionalitymaybeaffected,forexample,theabilitytopurchaseaproduct,buttherestofthesystemwouldcontinuetowork.
Also,imagineifwesuddenlyrealizedthatthedatabaseortheprogramminglanguageweusedtoimplementacomponentwasnotagooddesigndecision.Inamonolithicapplication,therewouldbeverylittlewecoulddotochangethingswithoutaffectingtheentiresystem;instead,inaMicroservicearchitecture,wecouldmoreeasilyre-implementtheentireservicefromscratch,usingadifferentdatabaseorplatform,andtherestofthesystemwouldnotevennoticeit.
Reusabilityacrossplatformsandlanguages
Splittingabigmonolithicapplicationintomanysmallservicesallowsustocreateindependentunitsthatcanbere-usedmuchmoreeasily.Elasticsearch(http://www.elasticsearch.org)isagreatexampleofare-usablesearchservice,alsotheauthenticationserverwebuiltinChapter5,WiringModules,isanotherexampleofaservicethatcanbeeasilyre-usedinanyapplication,regardlessoftheprogramminglanguageit'sbuiltin.
Themainadvantageisthatthelevelofinformationhidingisusuallymuchhighercomparedtomonolithicapplications.Thisispossiblebecausetheinteractionsusuallyhappenthrougharemoteinterfacesuchasawebserviceoramessagebroker,whichmakesitmucheasiertohidetheimplementationdetailsandshieldtheclientfromchangesinthewaytheserviceisimplementedordeployed.Forexample,ifallwehavetodoisinvokeawebservice,weareshieldedfromthewaytheinfrastructurebehindisscaled,fromwhatprogramminglanguageituses,fromwhatdatabaseitusestostoreitsdata,andsoon.
Awaytoscaletheapplication
Goingbacktothescalecube,it'sclearthatmicroservicesareequivalenttoscalinganapplicationalongtheYaxis,soit'salreadyameansforthedistributionoftheloadacrossmultiplemachines.Also,weshouldnotforgetthatwecancombinemicroserviceswiththeothertwodimensionsofthecubetoscaletheapplicationevenfurther.Forexample,eachservicecouldbeclonedtohandlemoretraffic,andtheinterestingaspectisthattheycanbescaledindependently,allowingbetterresourcemanagement.
Thechallengesofmicroservices
Atthispoint,itwouldlooklikemicroservicesarethesolutiontoallourproblems;however,thisisfarfrombeingtrue.Infact,havingmorenodestomanageintroducesahighercomplexityintermsofintegration,deployment,andcodesharing;itfixessomeofthepainsoftraditionalarchitecturesbutitalsoopensmanynewquestions.Howdowemaketheservicesinteract?Howcanwedeploy,scale,andmonitorsuchahighnumberofapplications?Howcanweshareandreusecodebetweenservices?Fortunately,cloudservicesandmodernDevOpsmethodologiescanprovidesomeanswerstothosequestions,andalso,Node.jscanhelpalot.Itsmodulesystemisaperfectcompaniontosharecodebetweendifferentprojects.Node.jswasmadetobeanodeinadistributedsystemsuchasthoseimplementedusingtheMicroservicearchitecture.
Tip
Althoughmicroservicescanbebuiltusinganyframework(orevenjustthecoreNode.jsmodules),thereareafewsolutionsspecializedforthispurpose,amongthemostnotablewehaveSeneca(https://npmjs.org/package/seneca).Ausefultooltomanagethedeploymentofmicroservicesisnscale(https://github.com/nearform/nscale).
IntegrationpatternsinaMicroservicearchitectureOneofthetoughestchallengesofmicroservicesisconnectingallthenodestogethertomakethemcollaborate.Forexample,theCartserviceofoure-commerceapplicationwouldmakelittlesensewithoutsomeProductstoadd,andtheCheckoutservicewouldbeuselesswithoutalistofproductstobuy(acart).Aswealreadymentioned,therearealsootherfactorsthatnecessitateaninteractionbetweenthevariousservices.Forexample,theSearchservicehastoknowwhichProductsareavailableandmustalsoensuretokeepitsinformationup-to-date.ThesamecanbesaidabouttheCheckoutservicethathastoupdatetheinformationaboutProductavailabilitywhenapurchaseiscompleted.
Whendesigninganintegrationstrategy,it'salsoimportanttoconsiderthecouplingthatit'sgoingtointroducebetweentheservicesinthesystem.Weshouldnotforgetthatdesigningadistributedarchitectureinvolvesthesamepracticesandprinciplesthatweuselocallywhendesigningamoduleorsubsystem,therefore,wealsoneedtotakeintoconsiderationpropertiessuchasthereusabilityandextensibilityoftheservice.
TheAPIproxyThefirstpatternwearegoingtoshowmakesuseofanAPIproxy,aserverthatproxiesthecommunicationsbetweenaclientandasetofremoteAPI.IntheMicroservicearchitecture,itsmainpurposeistoprovideasingleaccesspointformultipleAPIendpoints,butitcanalsoofferloadbalancing,caching,authentication,andtrafficlimiting,allfeaturesthatproveouttobeveryusefultoimplementasolidAPIsolution.
Thispatternshouldnotbenewtous;wealreadysawitinactionwhenwebuiltthecustomloadbalancerwithhttp-proxyandseaport.Forthatexample,ourloadbalancerwasexposingonlytwoservices,andthen,thankstoaServiceRegistry,itwasabletomapaURLpathtoaserviceandhencetoalistofservers.AnAPIproxyworksinthesameway;itisessentiallyareverseproxyandoftenalsoaloadbalancer,specificallyconfiguredtohandleAPIrequests.Thenextfigureshowshow
wecanapplysuchasolutiontooure-commerceapplication:
Fromtheprecedingfigure,itshouldbeclearhowanAPIproxycanhidethecomplexityofitsunderlyinginfrastructure.ThisisreallyhandyinaMicroserviceinfrastructure,asthenumberofnodesmaybehigh,especiallyifeachserviceisscaledacrossmultiplemachines.TheintegrationachievedbyanAPIProxyisthereforeonlystructural;thereisnosemanticmechanism.ItsimplyprovidesafamiliarmonolithicviewofacomplexMicroserviceinfrastructure.Thisisopposedtothenextpatternwearegoingtolearn,wheretheintegrationissemanticinstead.
APIorchestrationThepatternwearegoingtodescribenextisprobablythemostnaturalandexplicitwaytointegrateandcomposeasetofservices,andit'scalledAPIorchestration.DanielJacobson,VPofengineeringfortheNetflixAPI,inoneofhisblogposts(http://thenextweb.com/dd/2013/12/17/future-api-design-orchestration-layer),definesAPIOrchestrationasfollows:
AnAPIOrchestrationLayer(OL)isanabstractionlayerthattakesgenerically-modeleddataelementsand/orfeaturesandpreparestheminamorespecificway
foratargeteddeveloperorapplication.
Thegenericallymodeledelementsand/orfeaturesfitthedescriptionofaserviceinaMicroservicearchitectureperfectly.Theideaistocreateanabstractiontoconnectthosebitsandpiecestoimplementnewservicesspecifictotheapplication.
Let'smakeanexampleusingthee-commerceapplication.Refertothefollowingfigure:
TheprecedingfigureshowshowtheStorefront-endapplicationusesanOrchestrationlayertobuildmorecomplexandspecificfeaturesbycomposingandorchestratingexistingservices.ThedescribedscenariotakesasexampleahypotheticalcompleteCheckout()servicethatisinvokedthemomentacustomerclicksthePaybuttonattheendofthecheckout.ThefigureshowshowcompleteCheckout()isacompositeoperationmadeofthreedifferentsteps:
1. First,wecompletethetransactionbyinvokingcheckoutService/pay.2. Then,whenthepaymentissuccessfullyprocessed,weneedtotellthecart
servicethattheitemswerepurchasedandtheycanberemovedfromthecart.WedothatbyinvokingcartService/delete.
3. Also,whenthepaymentiscomplete,weneedtoupdatetheavailabilityofthe
productsthatwerejustpurchased.ThisisdonethroughproductsService/update.
Aswecansee,wetookthreeoperationsfromthreedifferentservicesandwebuiltanewAPIthatcoordinatestheservicestomaintaintheentiresysteminaconsistentstate.
AnothercommonoperationperformedbytheAPIOrchestrationLayerisdataaggregation,inotherwords,combiningdatafromdifferentservicesintoasingleresponse.Imagineifwewantedtolistalltheproductscontainedinacart.Inthiscase,theOrchestrationwouldneedtoretrievethelistofproductIDsfromtheCartserviceandthenretrievethecompleteinformationabouttheproductsfromtheProductsservice.Thewaysbywhichwecancombineandcoordinateservicesarereallyinfinite,buttheimportantpatterntorememberistheroleoftheOrchestrationlayer,whichactsasanabstractionbetweenanumberofservicesandaspecificapplication.
TheOrchestrationlayerisagreatcandidateforafurtherfunctionalsplitting.Itisinfactverycommontohaveitimplementedasadedicated,independentservice,inwhichcaseittakesthenameofAPIOrchestrator.ThispracticeisperfectlyinlinewiththeMicroservicephilosophy.
Thenextfigureshowsthisfurtherimprovementofourarchitecture:
CreatingastandaloneOrchestrator,asshowninthepreviousfigure,canhelpindecouplingtheclientapplication(inourcase,theStorefront-end)fromthecomplexityoftheMicroserviceinfrastructure.ThisremindsusabouttheAPIProxy;however,thereisacrucialdifference;anOrchestratorperformsasemanticintegrationofthevariousservices—it'snotjustanaïveproxy—anditoftenexposesanAPIthatisdifferentfromtheoneexposedbytheunderlyingservices.
IntegrationwithamessagebrokerTheOrchestratorpatterngaveusamechanismtointegratethevariousservicesinanexplicitway.Thishasbothadvantagesanddisadvantages.Itiseasytodesign,easytodebug,andeasytoscale,butunfortunately,ithastohaveacompleteknowledgeoftheunderlyingarchitectureandhoweachserviceworks.Ifweweretalkingaboutobjectsinsteadofarchitecturalnodes,theOrchestratorwouldbeananti-patterncalledGodObject,whichdefinesanobjectthatknowsanddoestoomuch,whichusuallyresultsinhighcoupling,lowcohesion,butmostimportantly,highcomplexity.
Thepatternwearenowgoingtoshowtriestodistributeacrosstheservicestheresponsibilityofsynchronizingtheinformationoftheentiresystem.However,thelastthingwewanttodoiscreatedirectrelationshipsbetweenservices,whichwouldresultinhighcouplingandafurtherincreaseinthecomplexityofthesystem,duetotheincreasingnumberofinterconnectionsbetweennodes.Thegoalistohaveeachservicemaintainitsisolation;theyshouldbeabletoworkevenwithouttherestoftheservicesinthesystemorincombinationwithnewservicesandnodes.
Thesolutionistouseamessagebroker,asystemcapableofdecouplingthesenderfromthereceiverofamessage,allowingtoimplementacentralizedpublish/subscribepattern,inpracticeanobserverpatternfordistributedsystems(wewilltalkmoreaboutthispatternlaterinthebook).Thefollowingdiagramshowsanexampleofhowthisappliestothee-commerceapplication:
Aswecansee,theclientoftheCheckoutservice,whichisthefront-endapplication,doesnotneedtocarryoutanyexplicitintegrationwiththeotherservices.AllithastodoisinvokecheckoutService/paytocompletethecheckoutandtakethemoneyfromthecustomer;alltheintegrationworkhappensinthebackground:
1. TheStorefront-endinvokesthecheckoutService/payoperationontheCheckoutservice.
2. Whentheoperationcompletes,theCheckoutservicegeneratesanevent,attachingthedetailsoftheoperation,thatis,thecartIdandthelistofproductsthatwerejustpurchased.Theeventispublishedintothemessagebroker.Atthispoint,theCheckoutservicedoesnotknowwhoisgoingtoreceivethemessage.
3. TheCartserviceissubscribedtothebroker,soit'sgoingtoreceivethepurchasedeventthatwasjustpublishedbytheCheckoutservice.TheCartservicereactsbyremovingfromitsdatabasethecartidentifiedwiththeIDcontainedinthemessage.
4. TheProductsservicewassubscribedtothemessagebrokeraswell,soitreceivesthesamepurchasedevent.Itthenupdatesitsdatabasebasedonthisnewinformation,adjustingtheavailabilityoftheproductsincludedinthemessage.
ThewholeprocesshappenswithoutanyexplicitinterventionfromexternalentitiessuchasanOrchestrator.Theresponsibilityforspreadingtheknowledgeandkeepinginformationinsyncisdistributedacrosstheservicesthemselves.ThereisnoGodservicethathastoknowhowtomovethegearsoftheentiresystem,eachserviceisinchargeofitsownpartoftheintegration.
Themessagebrokerisafundamentalelementtodecoupletheservicesandreducethecomplexityoftheirinteraction.Itmightalsoofferotherinterestingfeaturessuchaspersistentmessagequeuesandguaranteedorderingofthemessages.Wewilltalkmoreaboutthisinthenextchapter.
SummaryInthischapter,welearnedhowtodesignNode.jsarchitecturesthatscalebothincapacityandcomplexity.Wesawhowscalinganapplicationisnotonlyabouthandlingmoretrafficorreducingtheresponsetime,butit'salsoapracticetoapplywhenwewantbetteravailabilityandtolerancetofailures.Wesawhowthesepropertiesoftenareonthesamewavelengthandweunderstoodthatscalingearlyisnotabadpractice,especiallyinNode.js,whichallowsustodoiteasilyandwithfewresources.
Thescalecubetaughtusthatapplicationscanbescaledacrossthreedimensions.Wedivedintothetwomostimportantofthem,theXandYaxes,allowingustodiscovertwoessentialarchitecturalpatterns,namely,loadbalancingandmicroservices.WeshouldknowbynowhowtostartmultipleinstancesofthesameNode.jsapplication,howtodistributethetrafficacrossthem,andhowtoexploitthissetupforotherpurposessuchasfailtoleranceandzero-downtimerestarts.Wealsoanalyzedhowtohandletheproblemofdynamicandautoscaledinfrastructures,wesawthataServiceRegistrycancomereallyusefulinthosesituations.However,cloningandloadbalancingcoveronlyonedimensionofthescalecube,sowemovedouranalysistoanotherdimension,studyinginmoredetailwhatitmeanstosplitanapplicationbyitsconstituentservices,bybuildingaMicroservicearchitecture.Wesawhowmicroservicesenableacompleterevolutioninhowaprojectisdevelopedandmanaged,providinganaturalwaytodistributetheloadofanapplicationandsplititscomplexity.However,welearnedthatthisalsomeansshiftingthecomplexityfromhowtobuildabigmonolithicapplicationtohowtointegrateasetofservices.Thislastaspectiswherewefocusedthelastpartofouranalysis,showingsomeofthearchitecturalsolutionstointegrateasetofindependentservices.
Inthenextchapter,wewillhavethechancetoanalyzeinmoredetailthemessagingpatternswediscussedinthischapterinadditiontomoreadvancedintegrationtechniques,usefulwhenimplementingcomplexdistributedarchitectures.
Chapter8.MessagingandIntegrationPatternsIfscalabilityisaboutsplitting,systemsintegrationisaboutrejoining.Inthepreviouschapter,welearnedhowtodistributeanapplication,fragmentingitacrossseveralmachines.Inorderforittoworkproperly,allthosepieceshavetocommunicateinsomeway,andhence,theyhavetobeintegrated.
Therearetwomaintechniquestointegrateadistributedapplication:oneistouseasharedstoreasacentralcoordinatorandkeeperofalltheinformation,theotheroneistousemessagestodisseminatedata,events,andcommandsacrossthenodesofthesystem.Thislastoptioniswhatreallymakesthedifferencewhenscalingdistributedsystems,andit'salsowhatmakesthistopicsofascinatingandsometimescomplex.
Messagesareusedineverylayerofasoftwaresystem.WeexchangemessagestocommunicateontheInternet,wecanusemessagestosendinformationtootherprocessesusingpipes,wecanusemessageswithinanapplicationasanalternativetodirectfunctioninvocation(Commandpattern),andalsodevicedriversusemessagestocommunicatewiththehardware.Anydiscreteandstructureddatathatisusedasawaytoexchangeinformationbetweencomponentsandsystemscanbeseenasamessage.However,whendealingwithdistributedarchitectures,thetermmessagingsystemisusedtodescribeaspecificclassofsolutions,patterns,andarchitecturesthataremeanttofacilitatetheexchangeofinformationoverthenetwork.
Aswewillsee,thereareseveraltraitsthatcharacterizethesetypesofsystems.Wemightchoosetouseabrokerversusapeer-to-peerstructure,wemightusearequest/replyorone-waycommunication,orwemightusequeuestodeliverourmessagesmorereliably;thescopeofthetopicisreallybroad.Thebook,EnterpriseIntegrationPatterns,byGregorHohpeandBobbyWoolf,givesyouanideaaboutthevastnessofthetopic.ItisconsideredtheBibleofmessagingandintegrationpatternsthathasmorethan700pagesdescribing65differentintegrationpatterns.Thischapterexploresthemostimportantofthosewell-knownpatterns,consideringthemfromtheperspectiveofNode.jsanditsecosystem.
Tosumup,inthischapter,wewilllearnaboutthefollowingtopics:
ThefundamentalsofamessagingsystemThepublish/subscribepatternPipelinesandtaskdistributionpatterns
Request/replypatterns
FundamentalsofamessagingsystemWhentalkingaboutmessagesandmessagingsystems,therearefourfundamentalelementstotakeinconsideration,theseareasfollows:
Thedirectionofthecommunication,whichcanbeone-wayonlyorarequest/replyexchangeThepurposeofthemessage,whichalsodeterminesitscontentThetimingofthemessage,whichcanbesentandreceivedimmediatelyoratalatertime(asynchronously)Thedeliveryofthemessage,whichcanhappendirectlyorviaabroker
Inthesectionsthatwillfollow,wearegoingtoformalizetheseaspectsinordertoprovideabaseforourlaterdiscussions.
One-wayandrequest/replypatternsThemostfundamentalaspectinamessagingsystemisthedirectionofthecommunication,whichoftenalsodeterminesitssemantics.
Themostsimplecommunicationpatterniswhenthemessageispushed,one-wayfromasourcetoadestination,thisisatrivialsituation,anditdoesn'tneedmanyexplanations.
Atypicalexampleofone-waycommunicationisthee-mail,orawebserverthatsendsamessagetoaconnectedbrowserusingWebSockets,orasystemthatdistributestaskstoasetofworkers.
Therequest/replypatternis,however,farmorepopularthantheone-wayonlycommunication,atypicalexampleistheinvocationofawebservice.Thefollowingfigureshowsthissimpleandwell-knownscenario:
Therequest/replypatternmightseematrivialpatterntoimplement;however,wewillseethatitbecomesmorecomplicatedwhenthecommunicationisasynchronousorinvolvesmultiplenodes.Takealookattheexampleinthefollowingfigure:
Withthesetupshownintheprecedingdiagram,wecanappreciatethecomplexityofsomerequest/replypatterns.Ifweconsiderthedirectionofthecommunicationbetweenanytwonodes,wecansurelysaythatitisone-way.However,fromaglobalpointofview,theinitiatorsendsarequestandinturnreceivesanassociatedresponse,eveniffromadifferentnode.Inthesesituations,whatreallydifferentiatesarequest/replypatternfromabareone-wayloopistherelationshipbetweentherequestandthereply,whichiskeptintheinitiator.Thereplyisusuallyhandledinthesamecontextoftherequest.
MessagetypesAmessageisessentiallyameanstoconnectdifferentsoftwarecomponentsandtherearedifferentreasonsfordoingso:itmightbebecausewewanttoobtainsomeinformationheldbyanothersystemoracomponent,toexecuteoperationsremotely,ortonotifysomepeersthatsomethinghasjusthappened.Themessagecontentwillalsovarydependingonthereasonofthecommunication.Ingeneral,wecanidentifythreetypesofmessages,dependingontheirpurpose:
CommandMessage
EventMessageDocumentMessage
TheCommandMessageisalreadyfamiliartous;it'sessentiallyaserializedCommandObjectaswedescribeditinChapter4,DesignPatterns.Thepurposeofthistypeofmessageistotriggertheexecutionofanactionorataskonthereceiver.Forthistobepossible,ourmessagehastocontaintheessentialinformationtorunthetask,whichisusuallythenameoftheoperationandalistofargumentstoprovidewhenit'sexecuted.TheCommandMessagecanbeusedtoimplementRemoteProcedureCall(RPC)systems,distributedcomputations,ormoresimplyusedtorequestsomedata.RESTfulHTTPcallsaresimpleexamplesofcommands;eachHTTPverbhasaspecificmeaningandisassociatedwithapreciseoperation:GET,toretrievetheresource;POST,tocreateanewone;PUT,toupdateit;andDELETE,todestroyit.
AnEventMessageisusedtonotifyanothercomponentthatsomethinghasoccurred.Itusuallycontainsthetypeoftheeventandsometimesalsosomedetailssuchasthecontext,thesubjectoractorinvolved.Inwebdevelopment,weareusinganEventmessageinthebrowserwhenusinglong-pollingorWebSocketstoreceivenotificationsfromtheserverthatsomethinghasjusthappened,asforexample,changesinthedataoringeneral,thestateofthesystem.Theuseofeventsisaveryimportantintegrationmechanismindistributedapplications,asitenablesustokeepallthenodesofthesystemonthesamepage.
TheDocumentMessageisprimarilymeanttotransferdatabetweencomponentsandmachines.ThemaincharacteristicthatdifferentiatesaDocumentfromaCommand(whichmightalsocontaindata)isthatthemessagedoesnotcontainanyinformationthattellsthereceiverwhattodowiththedata.Ontheotherside,themaindifferencefromanEventmessageismainlytheabsenceofanassociationwithaparticularoccurrence,withsomethingthathappened.Often,therepliestotheCommandmessagesareDocumentmessages,astheyusuallycontainonlythedatathatwasrequestedortheresultofanoperation.
AsynchronousmessagingandqueuesAsNode.jsdevelopers,weshouldalreadyknowtheadvantagesofexecutingasynchronousoperations.Formessagingandcommunications,it'sthesamestory.
Wecancompareasynchronouscommunicationtoaphonecall:thetwopeersmustbeconnectedtothesamechannelatthesametimeandtheyshouldexchangemessagesinrealtime.Normally,ifwewanttocallsomeoneelse,weeitherneedanotherphoneorclosetheongoingcommunicationinordertostartanewone.
AnasynchronouscommunicationissimilartoanSMS,itdoesn'trequiretherecipienttobeconnectedtothenetworkthemomentwesendit,wemightreceivearesponseimmediatelyorafteranunknowndelay,orwemightnotreceivearesponseatall.WemightsendmultipleSMStomultiplerecipientsoneaftertheother,andreceivetheirresponses(ifany)inanyorder.Inshort,wehaveabetterparallelismwiththeuseoffewerresources.
Anotherimportantadvantageofasynchronouscommunicationsisthatthemessagescanbestoredandthendeliveredassoonaspossibleoratalatertime.Thismightbeusefulwhenthereceiveristoobusytohandlenewmessagesorwhenwewanttoguaranteethedelivery.Inmessagingsystems,thisismadepossibleusingamessagequeue,acomponentthatmediatesthecommunicationbetweenthesenderandthereceiver,storinganymessagebeforeitgetsdeliveredtoitsdestination,asshowninthefollowingfigure:
Ifforanyreasonthereceivercrashes,disconnectsfromthenetwork,orexperiencesaslowdown,themessagesareaccumulatedinthequeueanddispatchedassoonasthereceivercomesonlineandisfullyworking.Thequeuecanbelocatedinthesender,orsplitbetweenthesenderandreceiver,orlivinginadedicatedexternalsystemactingasamiddlewareforthecommunication.
Peer-to-peerorbroker-basedmessagingMessagescanbedelivereddirectlytothereceiver,inapeer-to-peerfashionorthroughacentralizedintermediarysystemcalledMessageBroker.Themainroleofthebrokeristodecouplethereceiverofthemessagefromthesender.Thefollowingfigureshowsthearchitecturaldifferencebetweenthetwoapproaches:
Inapeer-to-peerarchitecture,everynodeisdirectlyresponsibleforthedeliveryofthemessagetothereceiver.Thisimpliesthatthenodeshavetoknowtheaddressandportofthereceiverandtheyhavetoagreeonaprotocolandmessageformat.Thebrokereliminatesthesecomplexitiesfromtheequation:eachnodecanbetotallyindependentandcancommunicatewithanundefinednumberofpeerswithoutdirectlyknowingtheirdetails.Abrokercanalsoactasabridgebetweenthedifferentcommunicationprotocols,forexample,thepopularRabbitMQbroker(http://www.rabbitmq.com)supportsAdvancedMessageQueuingProtocol(AMQP),MessageQueueTelemetryTransport(MQTT),andSimple/StreamingTextOrientatedMessagingProtocol(STOMP),enablingmultipleapplicationssupportingdifferentmessagingprotocolstointeract.
NoteMQTT(http://mqtt.org)isalightweightmessagingprotocol,specificallydesignedformachine-to-machinecommunications(InternetofThings).AMQP(http://www.amqp.org)isamorecomplexprotocol,whichisdesignedtobeanopensourcealternativetoproprietarymessagingmiddlewares.STOMP(http://stomp.github.io)isalightweighttext-basedprotocol,whichcomesfromtheHTTPschoolofdesign.AllthreeareApplicationlayerprotocols,andbasedonTCP/IP.
Besidesthedecouplingandtheinteroperability,abrokercanoffermoreadvancedfeaturessuchaspersistentqueues,routing,messagetransformations,andmonitoring,withoutmentioningthebroadrangeofmessagingpatternsthatmanybrokerssupportoutofthebox.Ofcourse,nothingcanstopusfromimplementingall
thesefeaturesusingapeer-to-peerarchitecture,butunfortunatelythereismuchmoreeffortinvolved.Nonetheless,theremightbedifferentreasonstoavoidabroker:
RemovingasinglepointoffailureAbrokerhastobescaled,whileinapeer-to-peerarchitectureweonlyneedtoscalethesinglenodesExchangingmessageswithoutintermediariescangreatlyreducethelatencyofthetransmission
Ifwewanttoimplementapeer-to-peermessagingsystem,wecanalsohavemuchmoreflexibilityandpower,becausewearenotboundtoanyparticulartechnology,protocol,orarchitecture.ThepopularityofØMQ(http://zeromq.org),whichisalow-levellibraryforbuildingmessagingsystems,isagreatdemonstrationoftheflexibilitythatwecanhavebybuildingcustompeer-to-peerorhybridarchitectures.
Publish/subscribepatternPublish/subscribe(oftenabbreviatedPub/Sub)isprobablythebestknownone-waymessagingpattern.Weshouldalreadybefamiliarwithit,asit'snothingmorethanadistributedobserverpattern.Asinthecaseofobserver,wehaveasetofsubscribersregisteringtheirinterestinreceivingaspecificcategoryofmessages.Ontheotherside,thepublisherproducesmessagesthataredistributedacrossalltherelevantsubscribers.Thefollowingfigureshowsthetwomainvariationsofthepub/subpattern,thefirstpeer-to-peer,thesecondusingabrokertomediatethecommunication:
Whatmakespub/subsospecialisthefactthatthepublisherdoesn'tknowwhotherecipientsofthemessagesareinadvance.Aswesaid,it'sthesubscriberwhichhastoregisteritsinteresttoreceiveaparticularmessage,allowingthepublishertoworkwithanunknownnumberofreceivers.Inotherwords,thetwosidesofthepub/subpatternarelooselycoupled,whichmakesthisanidealpatterntointegratethenodesofanevolvingdistributedsystem.
Thepresenceofabrokerfurtherimprovesthedecouplingbetweenthenodesofthesystembecausethesubscribersinteractonlywiththebroker,notknowingwhichnodeisthepublisherofamessage.Aswewillseelater,abrokercanalsoprovideamessagequeuingsystem,allowingareliabledeliveryeveninthepresenceofconnectivityproblemsbetweenthenodes.
Now,let'sworkonanexampletodemonstratethispattern.
Buildingaminimalistreal-timechatapplicationToshowarealexampleofhowthepub/subpatterncanhelpusintegrateadistributedarchitecture,wearenowgoingtobuildaverybasicreal-timechatapplicationusingpureWebSockets.Then,wewilltrytoscaleitbyrunningmultipleinstancesandusingamessagingsystemtoputthemincommunication.
ImplementingtheserversideNow,let'stakeonestepatatime.Let'sfirstbuildourchatapplication;todothis,wewillrelyonthewspackage(https://npmjs.org/package/ws),whichisapureWebSocketimplementationforNode.js.Asweknow,implementingreal-timeapplicationsinNode.jsisprettysimple,andourcodewillconfirmthisassumption.Let'sthencreatetheserversideofourchat;itscontentisasfollows(intheapp.jsfile):
varWebSocketServer=require('ws').Server;
//staticfileserver
varserver=require('http').createServer(//[1]
require('ecstatic')({root:__dirname+'/www'})
);
varwss=newWebSocketServer({server:server});//[2]
wss.on('connection',function(ws){
console.log('Clientconnected');
ws.on('message',function(msg){//[3]
console.log('Message:'+msg);
broadcast(msg);
});
});
functionbroadcast(msg){//[4]
wss.clients.forEach(function(client){
client.send(msg);
});
}
server.listen(process.argv[2]||8080);
That'sit!That'sallweneedtoimplementourchatapplicationontheserver.Thisisthewayitworks:
1. WefirstcreateanHTTPserverandattachamiddlewarecalledecstatic(https://npmjs.org/package/ecstatic)toservestaticfiles.Thisisneededtoservetheclient-sideresourcesofourapplication(JavaScriptandCSS).
2. WecreateanewinstanceoftheWebSocketserverandweattachittoour
existingHTTPserver.WethenstartlisteningforincomingWebSocketconnections,byattachinganeventlistenerfortheconnectionevent.
3. Eachtimeanewclientconnectstoourserver,westartlisteningforincomingmessages.Whenanewmessagearrives,webroadcastittoalltheconnectedclients.
4. Thebroadcast()functionisasimpleiterationoveralltheconnectedclients,wherethesend()functionisinvokedoneachoneofthem.
ThisisthemagicofNode.js!Ofcourse,theserverthatweimplementedisveryminimalandbasic,butaswewillsee,itdoesitsjob.
ImplementingtheclientsideNext,it'stimetoimplementtheclientsideofourchat;thisisalsoaverysmallandsimplefragmentofcode,essentiallyaminimalHTMLpagewithsomebasicJavaScriptcode.Let'screatethispageinafilenamedwww/index.htmlasfollows:
<html>
<head>
<script>
varws=newWebSocket('ws://'+
window.document.location.host);
ws.onmessage=function(message){
varmsgDiv=document.createElement('div');
msgDiv.innerHTML=message.data;
document.getElementById('messages').appendChild(msgDiv);
};
functionsendMessage(){
varmessage=document.getElementById('msgBox').value;
ws.send(message);
}
</script>
</head>
<body>
Messages:
<divid='messages'></div>
<inputtype='text'placeholder='Sendamessage'
id='msgBox'>
<inputtype='button'onclick='sendMessage()'value='Send'>
</body>
</html>
TheHTMLpagewecreateddoesn'treallyneedmanycomments;itisjustapieceofstraightforwardwebdevelopment.WeusethenativeWebSocketobjecttoinitializeaconnectiontoourNode.jsserver,andthenstartlisteningformessagesfromtheserverdisplayingtheminnewdivelementsastheyarrive.Forsendingmessages,
instead,weuseasimpletextboxandabutton.
NoteWhenstoppingorrestartingthechatserver,theWebSocketconnectionisclosedanditwillnotreconnectautomatically(asitwouldusinghigh-levellibrariessuchasSocket.io).Thismeansthatitisnecessarytorefreshthebrowserafteraserverrestart,tore-establishtheconnection(orimplementareconnectionmechanism,whichwewillnotcoverhere).
RunningandscalingthechatapplicationWecantryrunningourapplicationimmediately,justlaunchtheserverwithacommandsuchasthefollowing:
nodeapp8080
NoteTorunthisdemo,youwillneedarecentbrowser,whichsupportsnativeWebSockets.Hereisalistofcompatiblebrowsers:http://caniuse.com/#feat=websockets
Pointingabrowsertohttp://localhost:8080shouldpresentaninterfacesimilartothefollowing:
Whatwewanttoshownow,iswhathappenswhenwetrytoscaleourapplicationbylaunchingmultipleinstances.Let'strytodothis,let'sstartanotherserveronanotherport:
nodeapp8081
Thedesiredoutcomeofscalingourchatapplicationshouldbethatthetwoclientsconnectingtothetwodifferentserversshouldbeabletoexchangechatmessages.Unfortunately,thisisnotwhathappenswithourcurrentimplementation,wecantrythatbyopeninganotherbrowsertabtohttp://localhost:8081.
Whensendingachatmessageononeinstance,webroadcastamessagelocally,distributingittoonlytheclientsconnectedtothatparticularserver.Inpractice,thetwoserversdon'ttalktoeachother.Weneedtointegratethem.
NoteInarealapplication,wewillusealoadbalancertodistributetheloadacrossourinstances,butforthisdemowewillnotuseone.Thisallowsustoaccesseachserverinadeterministicwaytoverifyhowitinteractswiththeotherinstances.
UsingRedisasamessagebrokerWestartouranalysisofthemostimportantpub/subimplementationsbyintroducingRedis(http://redis.io),whichisaveryfastandflexiblekey-valuestore,alsodefinedbymanyasadatastructureserver.Redisismoreadatabasethanamessagebroker,howeveramongitsmanyfeaturesthereisapairofcommandsspecificallydesignedtoimplementacentralizedpublish/subscribepattern.Ofcourse,thisimplementationisverysimpleandbasic,comparedtomoreadvancedmessage-orientedmiddleware,butthisisoneofthemainreasonsforitspopularity.Often,infact,Redisisalreadyavailableinanexistinginfrastructure,forexample,asacachingserverorsessionstore;itsspeedandflexibilitymakeitaverypopularchoiceforsharingdatainadistributedsystem.So,assoonastheneedforapublish/subscribebrokerarisesinaproject,themostsimpleandimmediatechoiceistoreuseRedisitself,avoidingtoinstallandmaintainadedicatedmessagebroker.Let'sworkonanexampletodemonstrateitssimplicityandpower.
NoteThisexamplerequiresaworkinginstallationofRedis,listeningonitsdefaultport.Youcanfindmoredetailsathttp://redis.io/topics/quickstart.
OurplanofactionistointegrateourchatserversusingRedisasamessagebroker.
Eachinstancepublishesanymessagereceivedfromitsclientstothebroker,andatthesametimeitsubscribesforanymessagecomingfromotherserverinstances.Aswecansee,eachserverinourarchitectureisbothasubscriberandapublisher.Thefollowingfigureshowsarepresentationofthearchitecturethatwewanttoobtain:
Bylookingattheprecedingfigure,wecansumupthejourneyofamessageasfollows:
1. Themessageistypedintothetextboxofthewebpageandsenttotheconnectedinstanceofourchatserver.
2. Themessageisthenpublishedtothebroker.3. Thebrokerdispatchesthemessagetoallthesubscribers,whichinour
architecturearealltheinstancesofthechatserver.4. Ineachinstance,themessageisdistributedtoalltheconnectedclients.
NoteRedisallowspublishingandsubscribingtochannels,whichareidentifiedbyastring,forexample,chat.nodejs.Italsoallowsustouseglob-stylepatternstodefinesubscriptionsthatcanpotentiallymatchmultiplechannels,forexample,chat.*.
Let'sseeinpracticehowthisworks.Let'smodifytheservercodebyaddingthepublish/subscribelogic:
varWebSocketServer=require('ws').Server;
varredis=require("redis");//[1]
varredisSub=redis.createClient();
varredisPub=redis.createClient();
//staticfileserver
varserver=require('http').createServer(
require('ecstatic')({root:__dirname+'/www'})
);
varwss=newWebSocketServer({server:server});
wss.on('connection',function(ws){
console.log('Clientconnected');
ws.on('message',function(msg){
console.log('Message:'+msg);
redisPub.publish('chat_messages',msg);//[2]
});
});
redisSub.subscribe('chat_messages');//[3]
redisSub.on('message',function(channel,msg){
wss.clients.forEach(function(client){
client.send(msg);
});
});
server.listen(process.argv[2]||8080);
Thechangesthatwemadetoouroriginalchatserverarehighlightedintheprecedingcode;thisishowitworks:
1. ToconnectourNode.jsapplicationtotheRedisserver,weusetheredispackage(https://npmjs.org/package/redis),whichisacompleteclientthatsupportsalltheavailableRediscommands.Next,weinstantiatetwodifferentconnections,oneusedtosubscribetoachannel,theothertopublishmessages.ThisisnecessaryinRedis,becauseonceaconnectionisputinsubscribermodeonlycommandsrelatedtothesubscriptioncanbeused.Thismeansthatweneedasecondconnectionforpublishingmessages.
2. Whenanewmessageisreceivedfromaconnectedclient,wepublishamessageinthechat_messageschannel.Wedon'tdirectlybroadcastthemessagetoourclientsbecauseourserverissubscribedtothesamechannel(aswewillseeinamoment),soitwillcomebacktousthroughRedis.Forthescopeofthisexample,thisisasimpleandeffectivemechanism.
3. Aswesaid,ourserveralsohastosubscribetothechat_messageschannel,soweregisteralistenertoreceiveallthemessagespublishedintothatchannel(eitherbythecurrentserveroranyotherchatserver).Whenamessageis
received,wesimplybroadcastittoalltheclientsconnectedtothecurrentWebSocketserver.
Thesefewchangesareenoughtointegrateallthechatserversthatwemightdecidetostart.Toprovethis,wecantrystartingmultipleinstancesofourapplication:
nodeapp8080
nodeapp8081
nodeapp8082
Wecanthenconnectmultiplebrowsers'tabstoeachinstanceandverifythatthemessageswesendtooneserveraresuccessfullyreceivedbyalltheotherclientsconnectedtodifferentservers.Congratulations!Wejustintegratedadistributedreal-timeapplicationusingthepublish/subscribepattern.
Peer-to-peerpublish/subscribewithØMQThepresenceofabrokercanconsiderablysimplifythearchitectureofamessagingsystem;however,therearecircumstanceswhereitisnotanoptimalsolution,asforexample,whenlatencyiscritical,whenscalingcomplexdistributedsystems,orwhenthepresenceofasinglepointoffailureisnotanoption.
IntroducingØMQIfourprojectfallsinthecategoryofpossiblecandidatesforapeer-to-peermessageexchange,thebestsolutiontoevaluateiscertainlyØMQ(http://zeromq.org,alsoknownaszmq,ZeroMQ,or0MQ);wealreadymentionedthislibraryearlierinthebook.ØMQisanetworkinglibrarythatprovidesthebasictoolstobuildalargevarietyofmessagingpatterns.Itislowlevel,extremelyfast,andhasaminimalisticAPIbutitoffersallthebasicbuildingblocksofamessagingsystemsuchasatomicmessages,loadbalancing,queues,andmanymore.Itsupportsmanytypesoftransportssuchasin-processchannels(inproc://),inter-processcommunication(ipc://),multicastusingthePGMprotocol(pgm://orepgm://),andofcourse,theclassicTCP(tcp://).
AmongthefeaturesofØMQ,wecanalsofindtoolstoimplementapublish/subscribepattern,exactlywhatweneedforourexample.So,whatwearegoingtodonowis,removethebroker(Redis)fromthearchitectureofourchatapplicationandletthevariousnodescommunicateinapeer-to-peerfashionleveragingthepublish/subscribesocketsofØMQ.
Note
AØMQsocketcanbeconsideredanetworksocketonsteroids,whichprovidesadditionalabstractionstohelpimplementthemostcommonmessagingpatterns.Forexample,wecanfindsocketsdesignedtoimplementpublish/subscribe,request/reply,orone-waycommunications.
Designingapeer-to-peerarchitectureforthechatserverWhenweremovethebrokerfromourarchitecture,eachinstanceofthechatapplicationhastodirectlyconnecttotheotheravailableinstancesinordertoreceivethemessagestheypublish.InØMQ,wehavetwotypesofsocketsspecificallydesignedforthispurpose:PUBandSUB.ThetypicalpatternistobindaPUBsockettoaportthatwillstartlisteningforsubscriptionscomingfromtheotherSUBsockets.
AsubscriptioncanhaveafilterthatspecifieswhatmessageswillbedeliveredtotheSUBsockets.Thefilterisasimplebinarybuffer(soitcanalsobeastring),whichwillbematchedagainstthebeginningofthemessage(whichisalsoabinarybuffer).WhenamessageissentthroughthePUBsocketitisbroadcasttoalltheconnectedSUBsockets,butonlyaftertheirsubscriptionfiltersareapplied.Thefilterswillbeappliedtothepublishersideonlyifaconnectedprotocolisused,asforexample,TCP.
Thefollowingfigureshowsusthepatternappliedtoourdistributedchatserverarchitecture(withonlytwoinstances,forsimplicity):
Theprecedingfigureshowsustheflowofinformationwhenwehavetwoinstancesof
thechatapplication,butthesameconceptcanbeappliedforNinstances.Thearchitecturetellsusthateachnodemustbeawareoftheothernodesinthesystem,tobeabletoestablishallthenecessaryconnections.ItalsoshowsushowthesubscriptionsgofromaSUBsockettoaPUBsocket,whilemessagestravelintheoppositedirection.
NoteToruntheexampleinthissection,youneedtoinstallthenativeØMQbinariesonyoursystem.Youcanfindmoreinformationathttp://zeromq.org/intro:get-the-software.Note:thisexamplewastestedagainstthe4.0branchofØMQ.
UsingtheØMQPUB/SUBsocketsLet'sseehowthisworksinpracticebymodifyingourchatserver(wewillshowyouonlythechangedparts):
[...]
varargs=require('minimist')(process.argv.slice(2));//[1]
varzmq=require('zmq');
varpubSocket=zmq.socket('pub');//[2]
pubSocket.bind('tcp://127.0.0.1:'+args['pub']);
varsubSocket=zmq.socket('sub');//[3]
varsubPorts=[].concat(args['sub']);
subPorts.forEach(function(p){
console.log('Subscribingto'+p);
subSocket.connect('tcp://127.0.0.1:'+p);
});
subSocket.subscribe('chat');
[...]
ws.on('message',function(msg){//[4]
console.log('Message:'+msg);
broadcast(msg);
pubSocket.send('chat'+msg);
});
[...]
subSocket.on('message',function(msg){//[5]
console.log('Fromotherserver:'+msg);
broadcast(msg.toString().split('')[1]);
});
[...]
server.listen(args['http']||8080);
Theprecedingcodeclearlyshowsthatthelogicofourapplicationbecameslightlymorecomplicated,howeverit'sstillstraightforwardconsideringthatweareimplementingadistributedandpeer-to-peerpublish/subscribepattern.Let'sseehowallthepiecescometogether:
1. Werequirethezmqpackage(https://npmjs.org/package/zmq),whichisessentiallytheNode.jsbindingfortheØMQnativelibrary.Wealsorequireminimist(https://npmjs.org/package/minimist),whichisacommand-lineargumentparser;weneedthistobeabletoeasilyacceptnamedarguments.
2. WeimmediatelycreateourPUBsocketandbindittotheportprovidedinthe--pubcommand-lineargument.
3. WecreatetheSUBsocketandweconnectittothePUBsocketsoftheotherinstancesofourapplication.TheportsofthetargetPUBsocketsareprovidedinthe--subcommand-linearguments(theremightbemorethanone).Wethencreatetheactualsubscription,byprovidingchatasafilter,whichmeansthatwewillreceiveonlythemessagesbeginningwithchat.
4. WhenanewmessageisreceivedbyourWebSocket,webroadcastittoalltheconnectedclientsbutwealsopublishitthroughourPUBsocket.Weusechatasaprefixfollowedbyaspace,sothemessagewillbepublishedtoallthesubscriptionsusingchatasafilter.
5. WestartlisteningformessagesthatarriveatourSUBsocket,wedosomesimpleparsingofthemessagetoremovethechatprefix,andthenwebroadcastittoalltheclientsconnectedtothecurrentWebSocketserver.
Wehavenowbuiltasimpledistributedsystem,integratedusingapeer-to-peerpublish/subscribepattern!
Let'sfireitup,let'sstartthreeinstancesofourapplicationbymakingsuretoconnecttheirPUBandSUBsocketsproperly:
nodeapp--http8080--pub5000--sub5001--sub5002
nodeapp--http8081--pub5001--sub5000--sub5002
nodeapp--http8082--pub5002--sub5000--sub5001
ThefirstcommandwillstartaninstancewithanHTTPserverlisteningontheport8080,whilebindingaPUBsocketonport5000andconnectingtheSUBsockettotheports5001and5002,whichiswherethePUBsocketsoftheothertwoinstancesshouldbelisteningat.Theothertwocommandsworkinasimilarway.
Now,thefirstthingwecansee,isthatØMQwillnotcomplainifaportcorrespondingtoaPUBsocketisnotavailable.Forexample,atthetimeofthefirstcommand,thereisnobodylisteningontheports5001and5002,howeverØMQisnotthrowinganyerror.ThisisbecauseØMQhasareconnectionmechanismthatwillautomaticallytrytoestablishaconnectiontotheseportsatregulartimeintervals.Thisfeaturealso
comesparticularlyhandyifanynodegoesdownorisrestarted.ThesameforgivinglogicappliestothePUBsocket:iftherearenosubscriptions,itwillsimplydropallthemessages,butitwillcontinueworking.
Atthispoint,wecantrytonavigatewithabrowsertoanyoftheserverinstancesthatwestartedandverifythatthemessagesareproperlybroadcasttoallthechatservers.
TipInthepreviousexample,weassumedastaticarchitecture,wherethenumberofinstancesandtheiraddressesareknowninadvance.WecanintroduceaServiceRegistry,asexplainedinthepreviouschapter,toconnectourinstancesdynamically.ItisalsoimportanttopointoutthatØMQcanbeusedtoimplementabrokerusingthesameprimitiveswedemonstratedhere.
DurablesubscribersAnimportantabstractioninamessagingsystemisthemessagequeue(MQ).Withamessagequeue,thesenderandthereceiver(s)ofthemessagedon'tnecessarilyneedtobeactiveandconnectedatthesametimetoestablishacommunication,becausethequeuingsystemtakescareofstoringthemessagesuntilthedestinationisabletoreceivethem.Thisbehaviorisopposedtothesetandforgetparadigm,whereasubscribercanreceivemessagesonlyduringthetimeitisconnectedtothemessagingsystem.
Asubscriberthatisabletoalwaysreliablyreceiveallthemessages,eventhosesentwhenit'snotlisteningforthem,iscalledadurablesubscriber.
TipTheMQTTprotocoldefinesalevelofQualityofService(QoS)forthemessagesexchangedbetweenthesenderandreceiver.Theselevelsarealsoveryusefultodescribethereliabilityofanyothermessagingsystem(notonlyMQTT).Theseareasfollows:
QoS0,atmostonce:Alsoknownassetandforget,themessageisnotpersisted,andthedeliveryisnotacknowledged.Thismeansthatthemessagecanbelostincasesofcrashesordisconnectionsofthereceiver.QoS1,atleastonce:Themessageisguaranteedtobereceivedatleastonce,
butduplicatesmightoccurif,forexample,thereceivercrashesbeforenotifyingthesender.Thisimpliesthatthemessagehastobepersistedintheeventualityithastobesentagain.QoS2,exactlyonce:ThisisthemostreliableQoS,itguaranteesthatthemessageisreceivedonceandonlyonce.Thiscomesattheexpenseofaslowerandmoredataintensivemechanismforacknowledgingthedeliveryofmessages.
FindoutmoreintheMQTTspecificationsathttp://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/mqtt-v3r1.html#qos-flows.
Aswesaid,toallowdurablesubscribers,oursystemhastouseamessagequeuetoaccumulatethemessageswhilethesubscriberisdisconnected.Thequeuecanbestoredinmemoryorpersistedondisktoallowtherecoveryofitsmessagesevenifthebrokerrestartsorcrashes.Thefollowingfigureshowsagraphicalrepresentationofadurablesubscriberbackedbyamessagequeue:
Thedurablesubscriberisprobablythemostimportantpatternenabledbyamessagequeue,butit'scertainlynottheonlyone,aswewillseelaterinthechapter.
TipTheRedispublish/subscribecommandsimplementasetandforgetmechanism(QoS0).However,Rediscanstillbeusedtoimplementadurablesubscriberusingacombinationofothercommands(withoutrelyingdirectlyonitspublish/subscribeimplementation).Youcanfindadescriptionofthistechniqueinthefollowingblogposts:
http://davidmarquis.wordpress.com/2013/01/03/reliable-delivery-message-queues-with-redishttp://www.ericjperry.com/redis-message-queue
ØMQdefinessomepatternstosupportdurablesubscribersaswell,butit'smostlyuptoustoimplementthismechanism.
IntroducingAMQPAmessagequeueisnormallyusedinallthosesituationswherenomessagecanbelost,whichincludesmission-criticalapplicationssuchasbankingorfinancialsystems.Thisusuallymeansthatthetypicalenterprise-grademessagequeueisaverycomplexpieceofsoftware,whichutilizesbulletproofprotocolsandpersistentstoragetoguaranteethedeliveryofthemessageeveninthepresenceofmalfunctions.Forthisreason,enterprisemessagingmiddlewarehavebeenformanyyearsaprerogativeofgiantssuchasOracleandIBM,eachoneofthemusuallyimplementingitsownproprietaryprotocolresultinginastrongcustomerlock-in.Fortunately,it'sbeenafewyearsnowthatmessagingsystemshaveenteredthemainstream,thankstothegrowthofopenprotocolssuchasAMQP,STOMP,andMQTT.Tounderstandhowamessagequeuingsystemworks,wearenowgoingtogiveanoverviewofAMQP;thisisfundamentaltounderstandhowtouseatypicalAPIbasedonthisprotocol.
AMQP(AdvancedMessageQueuingProtocol)isanopenstandardprotocolsupportedbymanymessagequeuingsystems.Besidesdefiningacommoncommunicationprotocol,italsoprovidesamodelfordescribingrouting,filtering,queuing,reliability,andsecurity.InAMQP,therearethreeessentialcomponents:
Queue:Thedatastructureresponsibleforstoringthemessagesconsumedbytheclients.Themessagesfromaqueuearepushed(orpulled)tooneormoreconsumers—essentiallyourapplications.Ifmultipleconsumersareattachedtothesamequeue,themessagesareloadbalancedacrossthem.Aqueuecanbeoneofthefollowing:
Durable:Thismeansthatthequeueisautomaticallyrecreatedifthebrokerrestarts.Adurablequeuedoesnotimplythatitscontentsarepreservedaswell;infact,onlymessagesthataremarkedaspersistentaresavedtothediskandrestoredincaseofarestart.Exclusive:Thismeansthatthequeueisboundtoonlyoneparticularsubscriberconnection.Whentheconnectionisclosed,thequeueisdestroyed.Auto-delete:Thiswillcausethequeuetobedeletedwhenthelastsubscriberdisconnects.
Exchange:Thisiswhereamessageispublished.AnExchangeroutesthemessagestooneormorequeuesdependingonthealgorithmitimplements:
Directexchange:Itroutesthemessagesbymatchinganentireroutingkey(forexample,chat.msg).Topicexchange:Itdistributesthemessagesusingaglob-likepatternmatchedagainsttheroutingkey(forexample,chat.#matchesalltheroutingkeysstartingwithchat).Fanoutexchange:Itbroadcastsamessagetoalltheconnectedqueues,ignoringanyroutingkeyprovided.
Binding:Thisisthelinkbetweenexchangesandqueues.Italsodefinestheroutingkeyorthepatternusedtofilterthemessagesthatarrivefromtheexchange.
Thesecomponentsaremanagedbyabroker,whichexposesanAPIforcreatingandmanipulatingthem.Whenconnectingtoabroker,aclientcreatesachannel—anabstractionofaconnection—whichisresponsibleformaintainingthestateofthecommunicationwiththebroker.
NoteInAMQP,thedurablesubscriberpatterncanbeobtainedbycreatinganytypeofqueuethatisnotexclusiveorauto-delete.
Thefollowingfigureshowsusallthesecomponentsputtogether:
TheAMQPmodeliswaymorecomplexthanthemessagingsystemswehaveusedsofar(RedisandØMQ);however,itoffersasetoffeaturesandareliabilitythatwouldbeveryhardtoobtainusingonlyprimitivepublish/subscribemechanisms.
TipYoucanfindadetailedintroductiontotheAMQPmodelontheRabbitMQwebsite:https://www.rabbitmq.com/tutorials/amqp-concepts.html.
DurablesubscriberswithAMQPandRabbitMQLet'snowpracticewhatwelearnedaboutdurablesubscribersandAMQPandworkonasmallexample.Atypicalscenariowhereit'simportanttonotloseanymessageiswhenwewanttokeepthedifferentservicesofaMicroservicearchitectureinsync;wealreadydescribedthisintegrationpatterninthepreviouschapter.Ifwewanttouseabrokertokeepallourservicesonthesamepage,it'simportantthatwedon'tloseanyinformation,otherwisewemightendupinaninconsistentstate.
Designingahistoryserviceforthechatapplication
Let'snowextendoursmallchatapplicationusingaMicroserviceapproach.Let'saddahistoryservicethatpersistsourchatmessagesinsideadatabase,sothatwhenaclientconnects,wecanquerytheserviceandretrievetheentirechathistory.WearegoingtointegratethehistoryservicewiththechatserverusingtheRabbitMQbroker(https://www.rabbitmq.com)andAMQP.
Thenextfigureshowsourplannedarchitecture:
Asdescribedintheprecedingarchitecture,wearegoingtouseasinglefanoutexchange;wedon'tneedanyparticularrouting,soourscenariodoesnotrequireanyexchangemorecomplexthanthat.Next,wewillcreateonequeueforeachinstanceofthechatserver;thesequeuesareexclusive,wearenotinterestedinreceivinganymissedmessagewhenachatserverisoffline,that'sthejobofourhistoryservice,whicheventuallycanalsoimplementmorecomplicatedqueriesagainstthestoredmessages.Inpractice,thismeansthatourchatserversarenotdurablesubscribers,andtheirqueueswillbedestroyedassoonastheconnectionisclosed.
Onthecontrary,thehistoryservicecannotaffordtoloseanymessage;otherwise,itwouldnotfulfillitsverypurpose.Thequeuewearegoingtocreateforithastobedurable,sothatanymessagethatispublishedwhilethehistoryserviceisdisconnectedwillbekeptinthequeueanddeliveredwhenitcomesbackonline.
WearegoingtousethefamiliarLevelUPasstorageengineforthehistoryservice,whilewewillusetheamqplibpackage(https://npmjs.org/package/amqplib)toconnecttoRabbitMQusingtheAMQPprotocol.
NoteThefollowingexamplerequiresaworkingRabbitMQserver,listeningonitsdefaultport.Formoreinformation,pleaserefertoitsofficialinstallationguideathttp://www.rabbitmq.com/download.html.
ImplementingareliablehistoryserviceusingAMQP
Let'snowimplementourhistoryservice!Wearegoingtocreateastandalone
application(atypicalMicroservice),whichisimplementedinthemodulehistorySvc.js.Themoduleismadeupoftwoparts:anHTTPservertoexposethechathistorytoclients,andanAMQPconsumerwhichisresponsibleforcapturingthechatmessagesandstoringtheminalocaldatabase.
Let'sseehowthislookslikeinthecodethatfollows:
varlevel=require('level');
vartimestamp=require('monotonic-timestamp');
varJSONStream=require('JSONStream');
vardb=level('./msgHistory');
varamqp=require('amqplib');
//HTTPserverforqueryingthechathistory
require('http').createServer(function(req,res){
res.writeHead(200);
db.createValueStream()
.pipe(JSONStream.stringify())
.pipe(res);
}).listen(8090);
varchannel,queue;
amqp
.connect('amqp://localhost')//[1]
.then(function(conn){
returnconn.createChannel();
})
.then(function(ch){
channel=ch;
returnchannel.assertExchange('chat','fanout');//[2]
})
.then(function(){
returnchannel.assertQueue('chat_history');//[3]
})
.then(function(q){
queue=q.queue;
returnchannel.bindQueue(queue,'chat');//[4]
})
.then(function(){
returnchannel.consume(queue,function(msg){//[5]
varcontent=msg.content.toString();
console.log('Savingmessage:'+content);
db.put(timestamp(),content,function(err){
if(!err)channel.ack(msg);
});
});
})
.catch(function(err){
console.log(err);
});
WecanimmediatelyseethatAMQPrequiresalittlebitofsetup,whichisnecessary
tocreateandconnectallthecomponentsofthemodel.It'salsointerestingtoobservethatamqplibsupportsPromisesbydefault,soweleveragedthemheavilytostreamlinetheasynchronousstepsoftheapplication.Let'sseeindetailhowitworks:
1. WefirstestablishaconnectionwiththeAMQPbroker,whichisRabbitMQinourcase.Then,wecreateachannel—whichissimilartoasession—thatwillmaintainthestateofourcommunications.
2. Next,wesetupourexchange,namedchat.Aswealreadymentioned,itisafanoutexchange.TheassertExchange()commandwillmakesurethattheexchangeexistsonthebroker,otherwiseitwillcreateit.
3. Wealsocreateourqueue,calledchat_history.Bydefault,thequeueisdurable—notexclusiveandnotauto-delete—sowedon'tneedtopassanyextraoptionstosupportdurablesubscribers.
4. Next,webindthequeuetotheexchangewepreviouslycreated.Here,wedon'tneedanyotherparticularoption,forexample,aroutingkeyorpattern,astheexchangeisofthetype,fanout,soitdoesn'tperformanyfiltering.
5. Finally,wecanbegintolistenformessagescomingfromthequeuewejustcreated.WesaveeverymessagethatwereceiveinaLevelDBdatabaseusingamonotonictimestampaskey(https://npmjs.org/package/monotonic-timestamp),tokeepthemessagessortedbydate.It'salsointerestingtoseethatweareacknowledgingeverymessageusingchannel.ack(msg),andonlyafterthemessageissuccessfullysavedintothedatabase.IftheACK(acknowledgment)isnotreceivedbythebroker,themessageiskeptinthequeueforbeingprocessedagain.ThisisanothergreatfeatureofAMQPforbringingthereliabilityofourservicetoawholenewlevel.Ifwearenotinterestedinsendingexplicitacknowledgments,wecanpasstheoption{noAck:true}tothechannel.consume()API.
IntegratingthechatapplicationwithAMQP
TointegratethechatserversusingAMQP,wehavetouseasetupverysimilartotheoneweimplementedinthehistoryservice,sowearenotgoingtorepeatithereinfull.However,it'sstillinterestingtoseehowthequeueiscreatedandhowanewmessageispublishedintotheexchange.Therelevantpartsofthenewapp.jsfile,arethefollowing:
[...]
.then(function(){
returnchannel.assertQueue('chat_srv_'+httpPort,
{exclusive:true});
})
[...]
ws.on('message',function(msg){
console.log('Message:'+msg);
channel.publish('chat','',newBuffer(msg));
});
[...]
Aswementioned,ourchatserverdoesnotneedtobeadurablesubscriber,asetandforgetparadigmisenough.Sowhenwecreateourqueue,wepasstheoption{exclusive:true}indicatingthatthequeueisscopedtothecurrentconnectionandthereforeitwillbedestroyedassoonasthechatservershutsdown.
Publishinganewmessageisalsoveryeasy,wesimplyhavetospecifythetargetexchange(chat)andaroutingkey,whichinourcaseisempty('')becauseweareusingafanoutexchange.
Wecannowrunourimprovedchatarchitecture,todothatlet'sstarttwochatserversandthehistoryservice:
nodeapp8080
nodeapp8081
nodehistorySvc
Itisnowinterestingtoseehowoursystem,andinparticularthehistoryservice,behavesincaseofdowntime.IfwestopthehistoryserverandcontinuetosendmessagesusingthewebUIofthechatapplication,wewillseethatwhenthehistoryserverisrestarted,itwillimmediatelyreceiveallthemessagesitmissed.Thisisaperfectdemonstrationofhowthedurablesubscriberpatternworks!
TipItisnicetoseehowtheMicroserviceapproachallowsoursystemtosurviveevenwithoutoneofitscomponents—thehistoryservice.Therewouldbeatemporaryreductionoffunctionality(nochathistoryavailable)butpeoplewouldstillbeabletoexchangechatmessagesinrealtime.Awesome!
PipelinesandtaskdistributionpatternsInChapter6,Recipes,welearnedhowtodelegatecostlytaskstomultiplelocalprocesses,buteventhoughthiswasaneffectiveapproach,itcannotbescaledbeyondtheboundariesofasinglemachine.Inthissection,wearegoingtoseehowit'spossibletouseasimilarpatterninadistributedarchitecture,usingremoteworkers,locatedanywhereinanetwork.
Theideaistohaveamessagingpatternthatallowsustospreadtasksacrossmultiplemachines.Thesetasksmightbeindividualchunksofworkorpiecesofabiggertasksplitusingadivideandconquertechnique.
Ifwelookatthelogicalarchitecturerepresentedinthefollowingfigure,weshouldbeabletorecognizeafamiliarpattern:
Aswecanseefromtheprecedingdiagram,thepublish/subscribepatternisnotsuitableforthistypeofapplication,asweabsolutelydon'twantatasktobereceivedbymultipleworkers.Whatweneedinstead,isamessagedistributionpatternsimilartoaloadbalancer,thatdispatcheseachmessagetoadifferentconsumer(alsocalledworker,inthiscase).Inthemessagingsystemsterminology,thispatternis
knownascompetingconsumers,fan-outdistribution,orventilator.
OneimportantdifferencewiththeHTTPloadbalancerswehaveseeninthepreviouschapter,isthat,here,theconsumershaveamoreactiverole.Infact,aswewillseelater,mostofthetimeit'snottheproducerthatconnectstotheconsumers,buttheyaretheconsumersthemselvesthatconnecttothetaskproducerorthetaskqueueinordertoreceivenewjobs.Thisisagreatadvantageinascalablesystemasitallowsustoseamlesslyincreasethenumberofworkerswithoutmodifyingtheproduceroradoptingaserviceregistry.
Also,inagenericmessagingsystem,wedon'tnecessarilyhavearequest/replycommunicationbetweentheproducerandworkers.Instead,mostofthetime,thepreferredapproachistouseaone-wayasynchronouscommunication,whichenablesabetterparallelismandscalability.Insuchanarchitecture,messagescanpotentiallytravelalwaysinonedirection,creatingpipelines,asshowninthefollowingfigure:
Pipelinesallowustobuildverycomplexprocessingarchitectureswithouttheburdenofasynchronousrequest/replycommunication,oftenresultinginlowerlatencyandhigherthroughput.Intheprecedingfigure,wecanseehowmessagescanbedistributedacrossasetofworkers(fan-out),forwardedtootherprocessingunits,andthenaggregatedintoasinglenode(fan-in),usuallycalledsink.
Inthissection,wearegoingtofocusonthebuildingblocksofthesekindsofarchitectures,byanalyzingthetwomostimportantvariations:peer-to-peerandbroker-based.
TipThecombinationofapipelinewithataskdistributionpatternisalsocalledparallel
pipeline.
TheØMQfan-out/fan-inpatternWehavealreadydiscoveredsomeofthecapabilitiesofØMQforbuildingpeer-to-peerdistributedarchitectures.Intheprevioussection,weusedPUBandSUBsocketstodisseminateasinglemessagetomultipleconsumers;nowwearegoingtoseehowit'spossibletobuildparallelpipelinesusinganotherpairofsocketscalledPUSHandPULL.
PUSH/PULLsocketsIntuitively,wecansaythatthePUSHsocketsaremadeforsendingmessages,whilethePULLsocketsaremeantforreceiving.Itmightseematrivialcombination;however,theyhavesomenicecharacteristicsthatmakethemperfectforbuildingone-waycommunicationsystems:
Bothcanworkinconnectmodeorbindmode.Inotherwords,wecanbuildaPUSHsocketandbindittoalocalportlisteningfortheincomingconnectionsfromaPULLsocket,orviceversa,aPULLsocketmightlistenforconnectionsfromaPUSHsocket.Themessagesalwaystravelinthesamedirection,fromPUSHtoPULL,it'sonlytheinitiatoroftheconnectionthatcanbedifferent.Thebindmodeisthebestsolutionfordurablenodes,asforexample,thetaskproducerandthesink,whiletheconnectmodeisperfectfortransientnodes,asforexample,thetaskworkers.Thisallowsthenumberoftransientnodestovaryarbitrarilywithoutaffectingthemoredurablenodes.IftherearemultiplePULLsocketsconnectedtoasinglePUSHsocket,themessagesareevenlydistributedacrossallthePULLsockets,inpractice,theyareloadbalanced(peer-to-peerloadbalancing!).Ontheotherhand,aPULLsocketthatreceivesmessagesfrommultiplePUSHsocketswillprocessthemessagesusingafairqueuingsystem,whichmeansthattheyareconsumedevenlyfromallthesources—around-robinappliedtoinboundmessages.ThemessagessentoveraPUSHsocketthatdoesn'thaveanyconnectedPULLsocket,donotgetlost;theyareinsteadqueuedupontheproduceruntilanodecomesonlineandstartspullingthemessages.
WearenowstartingtounderstandhowØMQisdifferentfromtraditionalWebservicesandwhyit'saperfecttoolforbuildinganykindofmessagingsystem.
BuildingadistributedhashsumcrackerwithØMQ
Now,it'stimetobuildasampleapplicationtoseeinactionthepropertiesofthePUSH/PULLsocketswejustdescribed.
Asimpleandfascinatingapplicationtoworkwithwouldbeahashsumcracker,asystemthatusesabruteforcetechniquetotrytomatchagivenhashsum(MD5,SHA1,andsoon)toeverypossiblevariationofcharactersofagivenalphabet.Thisisanembarrassinglyparallelworkload(http://en.wikipedia.org/wiki/Embarrassingly_parallel),whichisperfectforbuildinganexampledemonstratingthepowerofparallelpipelines.
Forourapplication,wewanttoimplementatypicalparallelpipelinewithanodetocreateanddistributetasksacrossmultipleworkers,plusanodetocollectalltheresults.ThesystemwejustdescribedcanbeimplementedinØMQusingthefollowingarchitecture:
Inourarchitecture,wehaveaventilatorgeneratingallthepossiblevariationsofcharactersinagivenalphabetanddistributingthemtoasetofworkers,whichinturncalculatethehashsumofeverygivenvariationandtrytomatchitagainstthehashsumgivenastheinput.Ifamatchisfound,theresultissenttoaresultscollectornode(sink).
Thedurablenodesofourarchitecturearetheventilatorandthesink,whilethetransientnodesaretheworkers.ThismeansthateachworkerconnectsitsPULLsockettotheventilatoranditsPUSHsockettothesink,thiswaywecanstartandstophowmanyworkerswewantwithoutchanginganyparameterintheventilatororthesink.
Implementingtheventilator
Now,let'sstarttoimplementoursystembycreatinganewmodulefortheventilator,inafilenamedventilator.js:
varzmq=require('zmq');
varvariationsStream=require('variations-stream');
varalphabet='abcdefghijklmnopqrstuvwxyz';
varbatchSize=10000;
varmaxLength=process.argv[2];
varsearchHash=process.argv[3];
varventilator=zmq.socket('push');//[1]
ventilator.bindSync("tcp://*:5000");
varbatch=[];
variationsStream(alphabet,maxLength)
.on('data',function(combination){
batch.push(combination);
if(batch.length===batchSize){//[2]
varmsg={searchHash:searchHash,variations:batch};
ventilator.send(JSON.stringify(msg));
batch=[];
}
})
.on('end',function(){
//sendremainingcombinations
varmsg={searchHash:searchHash,variations:batch};
ventilator.send(JSON.stringify(msg));
});
Toavoidgeneratingtoomanyvariations,ourgeneratorusesonlythelowercaselettersoftheEnglishalphabetandsetsalimitonthesizeofthewordsgenerated.Thislimitisprovidedininputasacommandlineargument(maxLength)togetherwiththehashsumtomatch(searchHash).Weusealibrarycalledvariations-stream(https://npmjs.org/package/variations-stream)togenerateallthevariationsusingastreaminginterface.
Butthepartthatwearemostinterestedinanalyzing,ishowwedistributethetasksacrosstheworkers:
1. WefirstcreateaPUSHsocketandwebindittothelocalport5000,thisiswherethePULLsocketoftheworkerswillconnecttoreceivetheirtasks.
2. Wegroupthegeneratedvariationsinbatchesof10,000itemseachandthenwecraftamessagethatcontainsthehashtomatchandthebatchofwordstocheck.Thisisessentiallythetaskobjectthattheworkerswillreceive.Whenweinvokesend()overtheventilatorsocket,themessagewillbepassedtothenextavailableworker,followingaround-robindistribution.
Implementingtheworker
Now,it'stimetoimplementtheworker(worker.js):
varzmq=require('zmq');
varcrypto=require('crypto');
varfromVentilator=zmq.socket('pull');
vartoSink=zmq.socket('push');
fromVentilator.connect('tcp://localhost:5000');
toSink.connect('tcp://localhost:5001');
fromVentilator.on('message',function(buffer){
varmsg=JSON.parse(buffer);
varvariations=msg.variations;
variations.forEach(function(word){
console.log('Processing:'+word);
varshasum=crypto.createHash('sha1');
shasum.update(word);
vardigest=shasum.digest('hex');
if(digest===msg.searchHash){
console.log('Found!=>'+word);
toSink.send('Found!'+digest+'=>'+word);
}
});
});
Aswesaid,ourworkerrepresentsatransientnodeinourarchitecture,thereforeitssocketsshouldconnecttoaremotenodeinsteadoflisteningfortheincomingconnections.That'sexactlywhatwedoinourworker,wecreatetwosockets:
APULLsocketthatconnectstotheventilator,forreceivingthetasks.APUSHsocketthatconnectstothesink,forpropagatingtheresults.
Besidesthis,thejobdonebyourworkerisverysimple:foreachmessagereceivedweiterateoverthebatchofwordsitcontains,thenforeachwordwecalculatetheSHA1checksumandwetrytomatchitagainstthesearchHashpassedwiththemessage.Whenamatchisfound,theresultisforwardedtothesink.
Implementingthesink
Forourexample,thesinkisaverybasicresultcollector,whichsimplyprintsthemessagesreceivedbytheworkerstotheconsole.Thecontentsofthefilesink.jsareasfollows:
varzmq=require('zmq')
varsink=zmq.socket('pull');
sink.bindSync("tcp://*:5001");
sink.on('message',function(buffer){
console.log('Messagefromworker:',buffer.toString());
});
It'sinterestingtoseethatthesink(astheventilator)isalsoadurablenodeofourarchitectureandthereforewebinditsPULLsocketinsteadofconnectingitexplicitlytothePUSHsocketoftheworkers.
Runningtheapplication
Wearenowreadytolaunchourapplication,let'sstartacoupleofworkersandthesink:
nodeworker
nodeworker
nodesink
Then,it'stimetostarttheventilator,specifyingthemaximumlengthofthewordstogenerateandtheSHA1checksumthatwewanttomatch.Thefollowingisasamplelistofarguments:
nodeventilator4f8e966d1e207d02c44511a58dccff2f5429e9a3b
Whentheprecedingcommandisrun,theventilatorwillstartgeneratingallthepossiblewordsthathavealengthofatmostfourcharacters,distributingthemtothesetofworkerswestarted,alongwiththechecksumweprovided.Theresultsofthecomputation,ifany,willappearintheterminalofthesinkapplication.
PipelinesandcompetingconsumersinAMQPIntheprevioussection,wesawhowaparallelpipelinecanbeimplementedinapeer-to-peercontext.Nowwearegoingtoexplorethispatternwhenappliedtoafully-fledgedmessagebroker,suchasRabbitMQ.
Point-to-pointcommunicationsandcompetingconsumersInapeer-to-peerconfiguration,apipelineisaverystraightforwardconcepttopictureinmind.Withamessagebrokerinthemiddlethough,therelationshipbetweenthevariousnodesofthesystemarealittlebithardertounderstand;thebrokeritselfactsasanintermediaryforourcommunicationsandoften,wedon'treallyknowwhoisontheothersidelisteningformessages.Forexample,whenwesendamessageusingAMQP,wedon'tdeliveritdirectlytoitsdestination,butinsteadtoanexchangeandthentoaqueue.Finally,itwillbeforthebrokertodecidewheretoroutethe
message,basedontherulesdefinedintheexchange,thebindings,andthedestinationqueues.
IfwewanttoimplementapipelineandataskdistributionpatternusingasystemlikeAMQP,wehavetomakesurethateachmessageisreceivedbyonlyoneconsumer,butthisisimpossibletoguaranteeifanexchangecanpotentiallybeboundtomorethanonequeue.Thesolutionthen,istosendamessagedirectlytothedestinationqueue,bypassingtheexchangealtogether,thiswaywecanmakesurethatonlyonequeuewillreceivethemessage.Thiscommunicationpatterniscalledpoint-to-point.
Onceweareabletosendasetofmessagesdirectlytoasinglequeue,wearealreadyhalf-waytoimplementingourtaskdistributionpattern.Infact,thenextstepcomesnaturally:whenmultipleconsumersarelisteningonthesamequeue,themessageswillbedistributedevenlyacrossthem,implementingafan-outdistribution.Inthecontextofmessagebrokers,thisisbetterknownastheCompetingConsumerspattern.
ImplementingthehashsumcrackerusingAMQPWejustlearnedthatexchangesarethepointinabrokerwhereamessageismulticasttoasetofconsumers,whilequeuesaretheplacewheremessagesareloadbalanced.Withthisknowledgeinmind,let'snowimplementourbruteforcehashsumcrackerontopofanAMQPbroker(asforexample,RabbitMQ).Thefollowingfiguregivesanoverviewofthesystemwewanttoobtain:
Aswediscussed,todistributeasetoftasksacrossmultipleworkersweneedtouse
asinglequeue.Intheprecedingfigure,wecalledthisthejobsqueue.Ontheothersideofthejobsqueue,wehaveasetofworkers,whicharecompetingconsumers,inotherwords,eachonewillpulladifferentmessagefromthequeue.Theresultisthatmultipletaskswillexecuteinparallelondifferentworkers.
Anyresultgeneratedbytheworkersispublishedintoanotherqueue,whichwecalledresultsqueue,andthenconsumedbytheresultscollector;thisisactuallyequivalenttoasink,orfan-indistribution.Intheentirearchitecture,wedon'tmakeuseofanyexchange,weonlysendmessagesdirectlytotheirdestinationqueue,implementingapoint-to-pointcommunication.
Implementingtheproducer
Let'sseehowtoimplementsuchasystem,startingfromtheproducer(thevariationsgenerator).Itscodeisidenticaltothesamplewehaveseenintheprevioussectionexceptforthepartsconcerningthemessageexchange.Theproducer.jsfilewilllookasfollows:
varamqp=require('amqplib');
[...]
varconnection,channel;
amqp
.connect('amqp://localhost')
.then(function(conn){
connection=conn;
returnconn.createChannel();
})
.then(function(ch){
channel=ch;
produce();
})
.catch(function(err){
console.log(err);
});
functionproduce(){
[...]
variationsStream(alphabet,maxLength)
.on('data',function(combination){
[...]
varmsg={searchHash:searchHash,variations:batch};
channel.sendToQueue('jobs_queue',
newBuf
fer(JSON.stringify(msg)));
[...]
}
})
[...]
}
Aswecansee,theabsenceofanyexchangeorbindingmakesthesetupofanAMQPcommunicationmuchsimpler.Intheprecedingcode,wedidn'tevenneedaqueue,asweareinterestedonlyinpublishingamessage.
Themostimportantdetailthough,isthechannel.sendToQueue()API,whichisactuallynewtous.Asitsnamesays,that'stheAPIresponsiblefordeliveringamessagestraighttoaqueue—jobs_queueinourexample—bypassinganyexchangeorrouting.
Implementingtheworker
Ontheothersideofthejobs_queuewehavetheworkerslisteningfortheincomingtasks.Let'simplementtheircodeinafilecalledworker.js,asfollows:
varamqp=require('amqplib');
[...]
varchannel,queue;
amqp
.connect('amqp://localhost')
.then(function(conn){
returnconn.createChannel();
})
.then(function(ch){
channel=ch;
returnchannel.assertQueue('jobs_queue');
})
.then(function(q){
queue=q.queue;
consume();
})
[...]
functionconsume(){
channel.consume(queue,function(msg){
[...]
variations.forEach(function(word){
[...]
if(digest===data.searchHash){
console.log('Found!=>'+word);
channel.sendToQueue('results_queue',
newBuffer('Found!'+digest+'=>'+word));
}
[...]
});
channel.ack(msg);
});
};
OurnewworkerisalsoverysimilartotheoneweimplementedintheprevioussectionusingØMQ,exceptforthepartrelatedtothemessageexchange.Intheprecedingcode,wecanseehowwefirstmakesurethatjobs_queueexistsandthenwestartlisteningforincomingtasksusingchannel.consume().Then,everytimeamatchisfound,wesendtheresulttothecollectorviaresults_queue,usingagainapoint-to-pointcommunication.
Ifmultipleworkersarestarted,theywillalllistenonthesamequeue,resultinginthemessagestobeloadbalancedbetweenthem.
Implementingtheresultcollector
Theresultscollectorisagainatrivialmodule,simplyprintinganymessagereceivedtotheconsole.Thisisimplementedinthecollector.jsfile,asfollows:
[...]
.then(function(ch){
channel=ch;
returnchannel.assertQueue('results_queue');
})
.then(function(q){
queue=q.queue;
channel.consume(queue,function(msg){
console.log('Messagefromworker:',
msg.content.toString());
});
})
[...]
Runningtheapplication
Noweverythingisreadytogiveournewsystematry,wecanstartbyrunningacoupleofworkers,whichwillbothconnecttothesamequeue(jobs_queue),sothateverymessagewillbeloadbalancedbetweenthem:
nodeworker
nodeworker
Then,wecanrunthecollectormoduleandthenproducer(byprovidingthemaximumwordlengthandthehashtocrack):
nodecollector
nodeproducer4f8e966d1e207d02c44511a58dccff2f5429e9a3b
Withthis,weimplementedamessagepipelineandthecompetingconsumerspatternusingnothingbutAMQP.
Request/replypatternsDealingwithamessagingsystemoftenmeansusingaone-wayasynchronouscommunication;publish/subscribeisaperfectexample.
One-waycommunicationscangiveusgreatadvantagesintermsofparallelismandefficiency,butalonetheyarenotabletosolveallourintegrationandcommunicationproblems.Sometimes,agoodoldrequest/replypatternmightjustbetheperfecttoolforthejob.Therefore,inallthosesituationswhereanasynchronousone-waychannelisallthatwehave,it'simportanttoknowhowtobuildanabstractionthatallowsustoexchangemessagesinarequest/replyfashion.That'sexactlywhatwearegoingtolearnnext.
CorrelationidentifierThefirstrequest/replypatternwearegoingtolearniscalledcorrelationidentifieranditrepresentsthebasicblockforbuildingarequest/replyabstractionontopofaone-waychannel.
Thepatternconsistsinmarkingeachrequestwithanidentifier,whichisthenattachedtotheresponsebythereceiver;thiswaythesenderoftherequestcancorrelatethetwomessagesandreturntheresponsetotherighthandler.Thiselegantlysolvestheprobleminpresenceofaone-wayasynchronouschannelwheremessagescantravelinanydirectionatanytime.Let'stakealookattheexampleinthefollowingfigure:
TheprecedingscenarioshowshowusingacorrelationIDallowsustomatcheachresponsewiththerightrequest,evenifthosearesentandthenreceivedinadifferentorder.
Implementingarequest/replyabstractionusingcorrelationidentifiersLet'snowstartworkingonanexamplebychoosingthemostsimpletypeoftheone-waychannels,onethatispoint-to-point(whichdirectlyconnectstwonodesofthesystem)andfull-duplex(messagescantravelinbothdirections).
Inthesimplechannelcategory,wecanfind,forexample,WebSockets:theyestablishapoint-to-pointconnectionbetweentheserverandbrowser,andthemessagescantravelinanydirection.Anotherexample,isthecommunicationchannelthatiscreatedwhenachildprocessisspawnedusingchild_process.fork(),weshouldalreadyknowaboutit,wesawthisAPIinChapter6,Recipes.Thischanneltooisasynchronous,itconnectstheparentonlywiththechildprocessanditallowsmessagestotravelinanydirection.Thisisprobablythemostbasicchannelofthiscategory,sothat'swhatwearegoingtouseinournextexample.
Theplanforthenextapplicationistobuildanabstractioninordertowrapthechannelcreatedbetweentheparentandchildprocesses.Thisabstractionshouldprovidearequest/replycommunicationbyautomaticallymarkingeachrequestwithacorrelation
identifierandthenmatchingtheIDofanyincomingreplyagainstthelistofrequesthandlersawaitingaresponse.
FromChapter6,Recipes,weshouldrememberthattheparentprocesscanaccessthechannelwiththechildusingtwoprimitives:
child.send(message)
child.on('message',callback)
Inasimilarway,thechildcanaccessthechanneltotheparentprocessusing:
process.send(message)
process.on('message',callback)
Thismeansthattheinterfaceofthechannelavailableintheparentisidenticaltotheoneavailableinthechild;thiswillallowustobuildacommonabstraction,sothattherequestscanbesentfromboththeendsofthechannel.
Abstractingtherequest
Let'sstartbuildingthisabstractionbyconsideringthepartresponsibleforsendingnewrequests,let'screateanewfilecalledrequest.js:
varuuid=require('node-uuid');
module.exports=function(channel){
varidToCallbackMap={};//[1]
channel.on('message',function(message){//[2]
varhandler=idToCallbackMap[message.inReplyTo];
if(handler){
handler(message.data);
}
});
returnfunctionsendRequest(req,callback){//[3]
varcorrelationId=uuid.v4();
idToCallbackMap[correlationId]=callback;
channel.send({
type:'request',
data:req,
id:correlationId
});
};
}
Thisishowourrequestabstractionworks:
1. Theonethatfollowsisaclosurecreatedaroundourrequestfunction.ThemagicofthepatternliesintheidToCallbackMapvariable,whichstorestheassociationbetweentheoutgoingrequestsandtheirreplyhandlers.
2. Assoonasthefactoryisinvoked,thefirstthingwedoisstartlisteningforincomingmessages.IfthecorrelationIDofthemessage(containedintheinReplyToproperty)matchesanyoftheIDscontainedintheidToCallbackMapvariable,weknowthatwejustreceivedareply,soweobtainthereferencetotheassociatedresponsehandlerandweinvokeitwiththedatacontainedinthemessage.
3. Finally,wereturnthefunctionwewillusetosendnewrequests.ItsjobistogenerateacorrelationIDusingthenode-uuidpackage(https://npmjs.org/package/node-uuid)andthenwraptherequestdatainanenvelopethatallowsustospecifythecorrelationIDandthetypeofthemessage.
That'sitfortherequestmodule;let'smovetothenextpart.
Abstractingthereply
Wearejustastepawayfromimplementingthefullpattern,solet'sseehowthecounterpartoftherequest.jsmoduleworks.Let'screateanotherfilecalledreply.js,whichwillcontaintheabstractionforwrappingthereplyhandler:
module.exports=function(channel){
returnfunctionregisterHandler(handler){
channel.on('message',function(message){//[1]
if(message.type!=='request')return;
handler(message.data,function(reply){
channel.send({//[2]
type:'response',
data:reply,
inReplyTo:message.id
});
});
});
};
}
Ourreplymoduleisagainafactorythatreturnsafunctiontoregisternewreplyhandlers.Thisiswhathappenswhenanewhandlerisregistered:
1. Westartlisteningforincomingrequestsandwhenwereceiveone,weimmediatelyinvokehandlerbypassingthedataofthemessageandacallbackfunctiontocollectthereplyfromthehandler.
2. Oncethehandlerhasdoneitswork,itwillinvokethecallbackthatweprovidedreturningbackitsreply.WethenbuildanenvelopebyattachingthecorrelationIDoftherequest(theinReplyToproperty),thenweputeverythingbackintothechannel.
TheamazingthingofthispatternisthatinNode.jsitcomesveryeasy,everythingforusisalreadyasynchronous,soanasynchronousrequest/replycommunicationbuiltontopofaone-waychannelisnotverydifferentfromanyotherasynchronousoperation,especiallyifwebuildanabstractiontohideitsimplementationdetails.
Tryingthefullrequest/replycycle
Now,wearereadytotryournewasynchronousrequest/replyabstraction.Let'screateasamplereplierinafilenamedreplier.js:
varreply=require('./reply')(process);
reply(function(req,callback){
setTimeout(function(){
callback({sum:req.a+req.b});
},req.delay);
});
Ourrepliersimplycalculatesthesumbetweenthetwonumbersreceivedandreturnstheresultafteracertaindelay(whichisalsospecifiedintherequest).Thiswillallowustoverifythattheorderoftheresponsescanalsobedifferentfromtheorderinwhichwesenttherequests,toconfirmthatourpatternisworking.
Thefinalsteptocompletethesampleistocreatetherequestorinafilenamedrequestor.js,whichalsohasthetaskofstartingthereplierusingchild_process.fork():
varreplier=require('child_process')
.fork(__dirname+'/replier.js');
varrequest=require('./request')(replier);
request({a:1,b:2,delay:500},function(res){
console.log('1+2=',res.sum);
replier.disconnect();
});
request({a:6,b:1,delay:100},function(res){
console.log('6+1=',res.sum);
});
Therequestorstartsthereplierandthenpassesitsreferencetoourrequestabstraction.Wethenrunacoupleofsamplerequestsandverifythatthecorrelation
withtheresponsetheyreceiveisright.
Totryoutthesample,simplylaunchtherequestor.jsmodule,theoutputshouldbesomethingsimilartothefollowing:
6+1=7
1+2=3
Thisconfirmsthatourpatternworksperfectlyfineandthattherepliesarecorrectlyassociatedwiththeirownrequests,nomatterinwhatordertheyaresentorreceived.
ReturnaddressThecorrelationidentifieristhefundamentalpatternforcreatingarequest/replycommunicationontopofaone-waychannel;however,it'snotenoughwhenourmessagingarchitecturehasmorethanonechannelorqueue,orwhentherecanbepotentiallymorethanonerequestor.Inthesesituations,inadditiontoacorrelationID,wealsoneedtoknowthereturnaddress,apieceofinformationwhichallowstherepliertosendtheresponsebacktotheoriginalsenderoftherequest.
ImplementingthereturnaddresspatterninAMQPInAMQP,thereturnaddressisthequeuewheretherequestorislisteningforincomingreplies.Becausetheresponseismeanttobereceivedbyonlyonerequestor,it'simportantthatthequeueisprivateandnotsharedacrossdifferentconsumers.Fromtheseproperties,wecaninferthatwearegoingtoneedatransientqueue,scopedtotheconnectionoftherequestorandthatthereplierhastoestablishapoint-to-pointcommunicationwiththereturnqueue,tobeabletodeliveritsresponses.
Thefollowingimagegivesusanexampleofthisscenario:
Tocreatearequest/replypatternontopofAMQP,allthatweneedtodoistospecifythenameoftheresponsequeueinthemessageproperties,thiswaythereplierknowswheretheresponsemessagehastobedelivered.Thetheoryseemsverystraightforward,solet'sseehowtoimplementthisinarealapplication.
Implementingtherequestabstraction
Let'snowbuildarequest/replyabstractionontopofAMQP.WewilluseRabbitMQasabroker,butanycompatibleAMQPbrokershoulddothejob.Let'sstartwiththerequestabstraction(implementedintheamqpRequest.jsmodule),wewillshowhereonlytherelevantparts.
Thefirstinterestingthingtoobserve,ishowwecreatethequeuetoholdtheresponses;thisisthecoderesponsibleforthat:
channel.assertQueue('',{exclusive:true});
Whenwecreatethequeue,wedon'tspecifyanyname,whichmeansthatarandomonewillbechosenforus;inadditiontothis,thequeueisexclusive,whichmeansthatit'sboundtotheactiveAMQPconnectionanditwillbedestroyedwhentheconnectioncloses.Thereisnoneedtobindthequeuetoanexchange,aswedon'tneedanyroutingordistributiontothemultiplequeues,thismeansthatthemessageshavetobedeliveredstraightintoourresponsequeue.
Next,let'sseehowwecangenerateanewrequest:
AMQPRequest.prototype.request=function(queue,message,
callback){
varid=uuid.v4();
this.idToCallbackMap[id]=callback;
this.channel.sendToQueue(queue,
newBuffer(JSON.stringify(message)),
{correlationId:id,replyTo:this.replyQueue}
);
}
Therequest()methodacceptsasinputthenameoftherequestsqueueandthemessagetosend.Aswelearnedintheprevioussection,weneedtogenerateacorrelationIDandassociateittothecallbackfunction.Finally,wesendthemessage,specifyingthecorrelationIdandthereplyTopropertyasmetadata.
It'sinterestingtoseethatforsendingthemessageweareusingthechannel.sentToQueue()APIinsteadofchannel.publish();thisisbecausewearenotinterestedinimplementinganypublish/subscribedistributionusingexchanges,butamorebasicpoint-to-pointdeliverystraightintothedestinationqueue.
TipInAMQP,wecanspecifyasetofproperties(ormetadata),tobepassedtotheconsumertogetherwiththemainmessage.
ThelastimportantpieceofouramqpRequestprototypeiswherewelistenforincomingresponses:
AMQPRequest.prototype._listenForResponses=function(){
varself=this;
returnthis.channel.consume(this.replyQueue,function(msg){
varcorrelationId=msg.properties.correlationId;
varhandler=self.idToCallbackMap[correlationId];
if(handler){
handler(JSON.parse(msg.content.toString()));
}
});
}
Intheprecedingcode,welistenformessagesonthequeuewecreatedexplicitlyforreceivingresponses,thenforeachincomingmessagewereadthecorrelationIDandwematchitagainstthelistofhandlersawaitingareply.Oncewehavethehandler,weonlyneedtoinvokeitbypassingthereplymessage.
Implementingthereplyabstraction
That'sitfortheamqpRequestmodule,nowit'stimetoimplementtheresponseabstractioninanewmodulenamedamqpReply.js.
Here,wehavetocreatethequeuethatwillreceivetheincomingrequests;wecanuseasimpledurablequeueforthispurpose.Wewon'tshowthispart,sinceit'sagainallAMQPboilerplate.Whatweareinterestedinseeinginstead,ishowwehandlearequestandthensenditbacktotherightqueue:
AMQPReply.prototype.handleRequest=function(handler){
varself=this;
returnself.channel.consume(self.queue,function(msg){
varcontent=JSON.parse(msg.content.toString());
handler(content,function(reply){
self.channel.sendToQueue(
msg.properties.replyTo,
newBuffer(JSON.stringify(reply)),
{correlationId:msg.properties.correlationId}
);
self.channel.ack(msg);
});
});
}
Whensendingbackareply,weusechannel.sendToQueue()topublishthemessagestraightintothequeuespecifiedinthereplyTopropertyofthemessage(ourreturnaddress).AnotherimportanttaskofouramqpReplyobjectistosetacorrelationIdinthereply,sothatthereceivercanmatchthemessagewiththelistofpendingrequests.
Implementingtherequestorandthereplier
Everythingisnowreadytogiveoursystematry,butfirst,let'sbuildasamplerequestorandrepliertoseehowtouseournewabstraction.
Let'sstartfromthemodulereplier.js:
varReply=require('./amqpReply');
varreply=Reply('requests_queue');
reply.initialize().then(function(){
reply.handleRequest(function(req,callback){
console.log('Requestreceived',req);
callback({sum:req.a+req.b});
});
});
It'snicetoseehowtheabstractionwebuiltallowsustohideallthemechanismsthathandlethecorrelationIDandthereturnaddress;allweneedtodoistoinitializeanewreplyobject,specifyingthenameofthequeuewherewewanttoreceiveourrequests('requests_queue').Therestofthecodeisjusttrivial;oursamplerepliersimplycalculatesthesumofthetwonumbersreceivedasinputandsendsbacktheresultusingtheprovidedcallback.
Ontheotherside,wehaveasamplerequestorimplementedintherequestor.jsfile:
varreq=require('./amqpRequest')();
req.initialize().then(function(){
for(vari=100;i>0;i--){
sendRandomRequest();
}
});
functionsendRandomRequest(){
vara=Math.round(Math.random()*100);
varb=Math.round(Math.random()*100);
req.request('requests_queue',{a:a,b:b},
function(res){
console.log(a+'+'+b+'='+res.sum);
}
);
}
Oursamplerequestorsends100randomrequeststothe'requests_queue'queue.Inthiscasetoo,it'sinterestingtoseethatourabstractionisdoingitsjobperfectly,hidingallthedetailsoftheasynchronousrequest/replypattern.
Now,totryoutthesystem,simplyruntherepliermodulefollowedbyrequestormodule:
nodereplier
noderequestor
Wewillseeasetofoperationspublishedbytherequestorandthenreceivedbythereplier,whichinturnwillsendbacktheresponses.
Now,wecantryotherexperiments.Oncethereplierisstartedforthefirsttime,itcreatesadurablequeue;thismeansthat,ifwenowstopitandthenruntherequestoragain,thennorequestwillbelost.Allthemessageswillbestoredinthequeueuntilthereplierisstartedagain!
AnothernicefeaturethatwegetforfreeusingAMQPisthefactthatourreplierisscalableout-of-the-box.Totestthisassumption,wecantrytostarttwoormore
instancesofthereplier,andwatchtherequestsbeingloadbalancedbetweenthem.Thisworksbecause,everytimearequestorstarts,itattachesitselfasalistenertothesamedurablequeue,andasaresult,thebrokerwillloadbalancethemessagesacrossalltheconsumersofthequeue(competingconsumerspattern).Sweet!
TipØMQhasapairofsocketsspecificallymeantforimplementingrequest/replypatterns(REQ/REP),howevertheyaresynchronous(onlyonerequest/responseatatime).Morecomplexrequest/replypatternsarepossiblewithmoresophisticatedtechniques.Formoreinformation,youcanreadtheofficialguideathttp://zguide.zeromq.org/page:all#advanced-request-reply.
SummaryWereachedtheendofthischapter.Here,welearnedthemostimportantmessagingandintegrationpatternsandtheroletheyplayinthedesignofdistributedsystems.Wemadeouracquaintancewiththethreemajortypesofmessageexchangepatterns:publish/subscribe,pipelines,andrequest/reply,andwesawhowtheycanbeimplementedusingapeer-to-peerarchitectureoramessagebroker.Weanalyzedtheirprosandcons,andwesawthatbyusingAMQPandafull-fledgedmessagebroker,wecanimplementreliableandscalableapplicationswithlittledevelopmenteffortbutatacostofhavingonemoresystemtomaintainandscale.Also,wesawhowØMQallowsustobuilddistributedsystemswherewecanhavetotalcontrolovereveryaspectofthearchitecture,finetuningitspropertiesaroundourveryownrequirements.
Thischapteralsoclosesthebook,bynowweshouldhaveatoolbeltfullofpatternsandtechniqueswecangoandapplyinourprojects.WeshouldalsohaveamoredeepunderstandingofhowNode.jsdevelopmentworksandwhatareitsstrengthsandweaknesses.Throughoutthebook,wealsohadthechancetoworkwithamyriadofpackagesandsolutionsdevelopedbymanyextraordinarydevelopers.Attheend,thisisthemostbeautifulaspectofNode.js,itspeople,acommunitywhereeverybodyplaysitspartingivingbacksomething.
Ihopeyouenjoyedmysmallcontribution.
IndexA
Adaptee/AdapterAdapterpattern
about/AdapterLevelUP,usingthroughfilesystemAPI/UsingLevelUPthroughthefilesystemAPIexamples/Inthewild
add()function
about/Synchronouscontinuation-passingstyle
AdvancedMessageQueuingProtocol(AMQP)
about/Peer-to-peerorbroker-basedmessagingURL/Peer-to-peerorbroker-basedmessaging
advisedmethod/Proxyintheecosystem–functionhooksandAOPaggressivecachingpattern/AsynchronousrequestcachingAMQP
about/IntroducingAMQPqueue/IntroducingAMQPexchange/IntroducingAMQPbinding/IntroducingAMQPusing/DurablesubscriberswithAMQPandRabbitMQreal-timechatapplication,integrating/IntegratingthechatapplicationwithAMQP
amqplibpackage
URL/Designingahistoryserviceforthechatapplication
AngularJS
URL/DeclaringasetofdependenciestoaDIcontainer
Apacheab
URL/BuildingasimpleHTTPserver
APIorchestration
about/APIorchestrationdataaggregation/APIorchestration
APIOrchestrator/APIorchestrationAPIproxy/TheAPIproxyAPIs,ES6promises
constructor/Promises/A+implementationsstaticmethods,ofPromiseobject/Promises/A+implementationsmethods,ofPromiseinstance/Promises/A+implementations
Application-controlledextension
versusPlugin-controlled/Plugin-controlledvsApplication-controlledextension
appmodule
about/Theappmodule
args-listmodule
URL/RefactoringtheauthenticationservertouseaDIcontainer
arguments,constructor
resolve(obj)/Promises/A+implementationsreject(err)/Promises/A+implementations
AspectOrientedProgramming(AOP)/Proxyintheecosystem–functionhooksandAOPasync.queue()function
about/Limitedparallelexecution
asynchronousbatching
about/Asynchronousbatchingandcaching
asynchronouscaching
about/Asynchronousbatchingandcaching
asynchronouscallbackpattern
about/Synchronousorasynchronous?unpredictablefunction/AnunpredictablefunctionZalgo,unleashing/UnleashingZalgounpredictablefunction,using/UnleashingZalgosynchronousAPIs,using/UsingsynchronousAPIsexecution,deferring/Deferredexecution
asynchronouscontrolflow,streams
about/Asynchronouscontrolflowwithstreamssequentialexecution/Sequentialexecutionunorderedparallelexecution/Unorderedparallelexecutionunorderedlimitedparallelexecution/Unorderedlimitedparallelexecution
asynchronouscontrolflow,withgenerators
about/Asynchronouscontrolflowwithgeneratorsgenerator-basedcontrolflow,usingco/Generator-basedcontrolflowusingco
asynchronousfunction
about/Asynchronouscontinuation-passingstyle
asynchronouslyinitializedmodules
requiring/Requiringasynchronouslyinitializedmodulescanonicalsolutions/Canonicalsolutionspreinitializationqueues/Preinitializationqueuesimplementing/Implementingamodulethatinitializesasynchronously
asynchronousmessaging
about/Asynchronousmessagingandqueues
AsynchronousModuleDefinition(AMD)/UniversalModuleDefinitionasynchronousprogramming
challenges/Thedifficultiesofasynchronousprogramming
asynchronousrequestbatching
about/Asynchronousrequestbatchingbatchingrequests,intotalsaleswebserver/Batchingrequestsinthetotalsaleswebserver
asynchronousrequestcaching
about/Asynchronousrequestcachingcachingrequests,intotalsaleswebserver/Cachingrequestsinthetotalsaleswebserver
asynclibrary
about/Theasynclibraryreferencelink/Theasynclibrarysequentialexecution/Sequentialexecutionparallelexecution/Parallelexecutionlimitedparallelexecution/Limitedparallelexecutionpros/Comparisoncons/Comparison
authControllermodule
about/TheauthControllermodule,Refactoringtheauthenticationservertousedependencyinjection
authenticationserver
building,hardcodeddependenciesused/Buildinganauthenticationserverusinghardcodeddependenciesabout/Buildinganauthenticationserverusinghardcodeddependenciesrunning/Runningtheauthenticationserverrefactoring,fordependencyinjectionusage/Refactoringtheauthenticationservertousedependencyinjectionrefactoring,forservicelocatorusage/Refactoringtheauthenticationservertouseaservicelocatorrefactoring,forDIcontainerusage/RefactoringtheauthenticationservertouseaDIcontainer
authServicemodule
about/TheauthServicemodule,Refactoringtheauthenticationservertouseaservicelocator
availability,clustermodule/Resiliencyandavailabilitywiththeclustermodule
Bback-pressure
about/Back-pressure
back-pressure,writablestreams/Back-pressureBackboneModels
used,forsharingbusinesslogic/SharingbusinesslogicanddatavalidationusingBackboneModelsused,forsharingdatavalidation/SharingbusinesslogicanddatavalidationusingBackboneModelssharedmodels,implementing/Implementingthesharedmodelsplatform-specificcode,implementing/Implementingtheplatform-specificcodeisomorphicmodels,using/Usingtheisomorphicmodelsapplication,running/Runningtheapplication
BackboneViews
URL/SharingbusinesslogicanddatavalidationusingBackboneModels
basicconcepts,generators
about/Thebasicssimpleexample/Asimpleexamplegenerators,asiterators/Generatorsasiteratorsvalues,returningtogenerator/Passingvaluesbacktoagenerator
batching
about/Asynchronousrequestbatching
batching,withPromises/BatchingandcachingwithPromisesbatchoperations
URL/ImplementingaLevelUPplugin
binding,AMQP
about/IntroducingAMQP
Bluebird
URL/Promises/A+implementations
Bower
URL/TheadvantagesofusingBrowserify
brew
URL/LoadbalancingwithNginx
browser
code,sharingwith/Sharingcodewiththebrowser
Browserify
about/ConsiderationsontheUMDpattern,IntroducingBrowserifyURL/IntroducingBrowserify,TheadvantagesofusingBrowserifydemonstrating/ExploringthemagicofBrowserifyadvantages/TheadvantagesofusingBrowserify
Browserify,invokingfromGrunt
URL/TheadvantagesofusingBrowserify
Browserify,invokingfromGulp
URL/TheadvantagesofusingBrowserify
buffering
versusstreaming/BufferingvsStreaming
bundle
about/IntroducingBrowserify
bunyan
URL/Inthewild
businesslogic
sharing,BackboneModelsused/SharingbusinesslogicanddatavalidationusingBackboneModels
busy-waiting
about/Non-blockingI/O
Ccaching
about/Asynchronousbatchingandcaching
caching,withPromises/BatchingandcachingwithPromisescachingmechanisms
implementing/Notesaboutimplementingcachingmechanisms
callbackfunction
about/Thereactorpattern
callbackhell
about/Thecallbackhell
callbackpattern
about/Thecallbackpatterncontinuation-passingstyle/Thecontinuation-passingstyleasynchronous/Synchronousorasynchronous?synchronous/Synchronousorasynchronous?Node.jscallbackconventions/Node.jscallbackconventions
canonicalsolutions
about/Canonicalsolutions
centralregistry
about/Servicelocator
ChainofResponsibilitypattern/Middlewareasapatternchallenges,asynchronousprogramming
simplewebspider,creating/Creatingasimplewebspidercallbackhell/Thecallbackhell
chancemodule
URL/ImplementingReadablestreams
channels
about/Multiplexinganddemultiplexing,UsingRedisasamessagebroker,IntroducingAMQP
Checkoutservice/Integrationwithamessagebrokerchildprocess
URL/Communicatingwithachildprocess
childprocesses
about/Usingmultipleprocesses
child_process.fork()
URL/Runningthemux/demuxapplication
child_processAPI
URL/Communicatingwiththeparentprocess
cloning/Cloningandloadbalancingclosures
about/Thecallbackpatternreferencelink/Thecallbackpattern
Cloud-basedproxies/Scalingwithareverseproxyclustermodule
about/Theclustermodulebehavior/NotesonthebehavioroftheclustermoduleURL/NotesonthebehavioroftheclustermodulesimpleHTTPserver,building/BuildingasimpleHTTPserversimpleHTTPserver,profiling/BuildingasimpleHTTPserverscaling/Scalingwiththeclustermoduleresiliency/Resiliencyandavailabilitywiththeclustermoduleavailability/Resiliencyandavailabilitywiththeclustermodulezero-downtimerestart/Zero-downtimerestart
co
referencelink/Generator-basedcontrolflowusingcoabout/Generator-basedcontrolflowusingcoyieldables/Generator-basedcontrolflowusingco
co-limiter
referencelink/Limitedparallelexecution
code
sharing,withbrowser/Sharingcodewiththebrowser
codeprofiler,Factorypattern
about/Buildingasimplecodeprofiler
cohesion
about/CohesionandCoupling
combine-streampackage
URL/Combiningstreams
combinedstream
advantages/Combiningstreamsimplementing/Implementingacombinedstream
Command-lineInterface(CLI)/GzippingusingabufferedAPICommandMessage
about/Messagetypes
Commandpattern
about/Commandadvantages/Commandflexiblepattern/Aflexiblepattern
CommonJSmodules
about/Node.jsmodulesexplained
CommonJSmodulesystem
about/Modulesanddependencies
competingconsumers
about/Point-to-pointcommunicationsandcompetingconsumers
competingconsumerspattern
about/Point-to-pointcommunicationsandcompetingconsumers
competingconsumerspattern
about/Pipelinesandtaskdistributionpatterns
competitiverace
about/Thepattern
complexapplications
decomposing/Decomposingcomplexapplicationsmonolithicarchitecture/Monolithicarchitecturemicroservicearchitecture/TheMicroservicearchitecturemicroservicearchitecture,integrationpatterns/IntegrationpatternsinaMicroservicearchitecture
Component
URL/TheadvantagesofusingBrowserify
composabilityapproach,streams
about/Gzippingusingstreams
/Composabilityconfigurationmanagertemplate/AconfigurationmanagertemplateconsistentReadSync()function
about/UsingsynchronousAPIs
constructor
about/Promises/A+implementations
arguments/Promises/A+implementations
constructorinjection
about/Thedifferenttypesofdependencyinjection
continuation-passingstyle
about/Thecontinuation-passingstylesynchronouscontinuation-passingstyle/Synchronouscontinuation-passingstyleasynchronouscontinuation-passingstyle/Asynchronouscontinuation-passingstylenoncontinuation-passingstylecallbacks/Noncontinuation-passingstylecallbacks
continuousdeliveryprocess/Zero-downtimerestartcorrelationidentifier
about/Correlationidentifierused,forimplementingrequest/replyabstraction/Implementingarequest/replyabstractionusingcorrelationidentifiers
CouchDB
URL/Sharingthestateacrossmultipleinstances
CouchUP
URL/IntroducingLevelUPandLevelDB
coupling
about/Modulesanddependencies,CohesionandCouplingtightlycoupled/CohesionandCouplinglooselycoupled/CohesionandCoupling
CPU-bound
about/RunningCPU-boundtasksinterleavingpattern,withsetImmediate/InterleavingwithsetImmediatemultipleprocesses,using/Usingmultipleprocesses
CPU-boundtasks
running/RunningCPU-boundtaskssubsetsumproblem,solving/Solvingthesubsetsumproblem
Cross-originresourcesharing(CORS)/Buildinganauthenticationserverusinghardcodeddependenciescross-platformcode/Sharingcodewiththebrowsercross-platformdevelopment
fundamentals/Fundamentalsofcross-platformdevelopmentdesignpatterns/Designpatternsforcross-platformdevelopment
Ddataownership/AnexampleoftheMicroservicearchitecturedatavalidation
sharing,BackboneModelsused/SharingbusinesslogicanddatavalidationusingBackboneModels
dbmodule
about/Thedbmodule,Refactoringtheauthenticationservertouseaservicelocator,Canonicalsolutions
Decoratorpattern
about/Decoratorimplementing,techniques/Techniquesforimplementingdecorators
deferreds
about/Promises/A+implementations
demultiplexer(demux)/Multiplexinganddemultiplexingdemultiplexing
about/Multiplexinganddemultiplexingserversideapplication,creating/Serverside–demultiplexing
DenialofService(DoS)/LimitedparallelexecutionDenialofService(DoS)attacks
about/Delegatingthesubsetsumtasktootherprocesses
dependencies
about/Modulesanddependencies
dependency
about/ThemostcommondependencyinNode.jsbetweenmodules/ThemostcommondependencyinNode.js
dependencygraph
about/Modulesanddependencies
dependencyhellproblem/Smallmodulesdependencyinjection(DI)
about/Dependencyinjectionpros/Prosandconsofdependencyinjectioncons/Prosandconsofdependencyinjectionused,forexposingservices/Exposingservicesusingdependencyinjection
dependencyinjection(DI),types
constructorinjection/Thedifferenttypesofdependencyinjectionpropertyinjection/Thedifferenttypesofdependencyinjection
dependencyInjectioncontainers
URL/ProsandconsofaDependencyInjectioncontainer
dependencyinjectioncontainers
about/Dependencyinjection,Prosandconsofdependencyinjection,Dependencyinjectioncontainerpros/ProsandconsofaDependencyInjectioncontainercons/ProsandconsofaDependencyInjectioncontainerused,forexposingservices/Exposingservicesusingadependencyinjectioncontainer
Derby
URL/Runningtheapplication
designpattern
Factory/FactoryProxy/ProxyDecorator/DecoratorAdapter/AdapterStrategy/StrategyState/StateTemplate/TemplateMiddleware/MiddlewareCommand/Command
designpatterns,cross-platformdevelopment
about/Designpatternsforcross-platformdevelopmentStrategy/Designpatternsforcross-platformdevelopmentTemplate/Designpatternsforcross-platformdevelopmentAdapter/Designpatternsforcross-platformdevelopmentProxy/Designpatternsforcross-platformdevelopmentObserver/Designpatternsforcross-platformdevelopmentDependencyInjection/Designpatternsforcross-platformdevelopmentServicelocator/Designpatternsforcross-platformdevelopment
DIcontainer
setofdependencies,declaringto/DeclaringasetofdependenciestoaDIcontainer
dimensions,scalability
XAxis/ThethreedimensionsofscalabilityYAxis/ThethreedimensionsofscalabilityZAxis/Thethreedimensionsofscalability
directstyle
about/Synchronouscontinuation-passingstyle
distributedhashsumcracker,withØMQ
building/BuildingadistributedhashsumcrackerwithØMQventilator,implementing/Implementingtheventilatorworker,implementing/Implementingtheworkersink,implementing/Implementingthesinkexecuting/Runningtheapplication
Dnode
URL/Inthewild
docpad
URL/Pluginsaspackages
DocumentMessage
about/Messagetypes
Don'tRepeatYourself(DRY)principle
about/Smallmodules
ducktyping
URL/Buildingasimplecodeprofiler
duplexer2module
URL/Combiningstreams
Duplexstream
about/Duplexstreamscreating/Duplexstreams
durablesubscribers
about/DurablesubscribersAMQP/IntroducingAMQPwithAMQP/DurablesubscriberswithAMQPandRabbitMQwithRabbitMQ/DurablesubscriberswithAMQPandRabbitMQreal-timechatapplication/Designingahistoryserviceforthechatapplication
dynamicloadbalancer
implementing,withhttp-proxy/Implementingadynamicloadbalancerwithhttp-proxyandseaportimplementing,withseaport/Implementingadynamicloadbalancerwithhttp-proxyandseaport
dynamicscaling/UsingaServiceRegistry
Eecstaticmiddleware
URL/Implementingtheserversideabout/Implementingtheserverside
elasticsearch
URL/Reusabilityacrossplatformsandlanguages
embarrassinglyparallelworkload
about/BuildingadistributedhashsumcrackerwithØMQreferencelink/BuildingadistributedhashsumcrackerwithØMQ
encapsulationmechanism/Amechanismtoenforceencapsulationenvelope
about/Abstractingtherequest
ES6
about/Promises
ES6promises
about/Promises/A+implementationsAPIs/Promises/A+implementations
ES6WeakMaps
URL/Amechanismtoenforceencapsulation
EventEmitterfunction
about/TheEventEmitteron(event,listener)method/TheEventEmitteronce(event,listener)method/TheEventEmitteremit(event,[arg1],[])method/TheEventEmitterremoveListener(event,listener)method/TheEventEmitterusing/CreateanduseanEventEmitter
creating/CreateanduseanEventEmitterfilereadevent/CreateanduseanEventEmitterfoundevent/CreateanduseanEventEmittererrorevent/CreateanduseanEventEmitter
EventEmitters
about/Extensionpoints
eventloop
about/Eventdemultiplexing
EventMessage
about/Messagetypes
eventnotificationinterface
about/Eventdemultiplexing
exchange,AMQP
about/IntroducingAMQPdirectexchange/IntroducingAMQPtopicexchange/IntroducingAMQPfanoutexchange/IntroducingAMQP
Express
URL/MiddlewareinExpressMiddleware/MiddlewareinExpress
express
URL/Buildinganauthenticationserverusinghardcodeddependencies,Pluginsaspackages
expressmiddleware
about/MiddlewareinExpresstasks/MiddlewareinExpress
extensionpoints
about/Extensionpoints
Ezel
URL/Runningtheapplication
Ffactoryinjection/ThedifferenttypesofdependencyinjectionFactorypattern
about/Factorygenericinterface,forcreatingobjects/Agenericinterfaceforcreatingobjectsencapsulationmechanism/Amechanismtoenforceencapsulationsimplecodeprofiler,building/BuildingasimplecodeprofilerDnode/InthewildRestify/Inthewild
fail-safesocket
implementing/Implementingabasicfail-safesocket
fairqueuingsystem
about/PUSH/PULLsockets
fan-outdistribution
about/Pipelinesandtaskdistributionpatterns
Fibers
referencelink/Comparison
flexiblepattern
taskpattern/Thetaskpatterncomplexcommand/Amorecomplexcommand
flowingmode,readablestreams/Theflowingmodeforever
URL/LoadbalancingwithNginx
forwardproxy
URL/Scalingwithareverseproxy
framework/Plugin-controlledvsApplication-controlledextensionfrom2package
URL/throughandfrom
frompackage/throughandfrom
URL/throughandfrom
fsmodule
createReadStream()method/GettingstartedwithstreamscreateWriteStream()method/Gettingstartedwithstreams
fstreampackage
URL/Creatingatarballfrommultipledirectories
functionhooking/Proxyintheecosystem–functionhooksandAOPfundamentals,cross-platformdevelopment
about/Fundamentalsofcross-platformdevelopmentruntimecodebranching/Runtimecodebranchingbuild-timecodebranching/Build-timecodebranching
Ggateway
about/Scalingwithareverseproxy
generators
about/Generatorsbasics/Thebasicsasynchronouscontrolflow,withgenerators/Asynchronouscontrolflowwithgeneratorssequentialexecution/Sequentialexecutionparallelexecution/Parallelexecutionlimitedparallelexecution/Limitedparallelexecution
pros/Comparisoncons/Comparison
GodObject/IntegrationwithamessagebrokerGrunt
URL/Exposingservicesusingdependencyinjectionabout/Exposingservicesusingdependencyinjection
gulp
URL/Pluginsaspackages
Hhandler
about/Thereactorpattern
HAProxy
URL/Scalingwithareverseproxy
hardcodeddependencies
used,forbuildingauthenticationserver/Buildinganauthenticationserverusinghardcodeddependenciespros/Prosandconsofhardcodeddependenciescons/Prosandconsofhardcodeddependenciesusing/Usinghardcodeddependencies
hardcodeddependency
about/Hardcodeddependency
Harmony
about/Promises
hashsumcracker,withAMQP
implementing/ImplementingthehashsumcrackerusingAMQPproducer,implementing/Implementingtheproducerworker,implementing/Implementingtheworker
resultcollector,implementing/Implementingtheresultcollectorexecuting/Runningtheapplication
Hollywoodprinciple
about/Plugin-controlledvsApplication-controlledextension
hooks
URL/Proxyintheecosystem–functionhooksandAOPabout/Extensionpoints
horizontalpartitioning/Thethreedimensionsofscalabilityhorizontalscaling/Cloningandloadbalancinghttp-proxy
URL/Inthewild,Implementingadynamicloadbalancerwithhttp-proxyandseaport
HTTPserver
building/BuildingasimpleHTTPserverprofiling/BuildingasimpleHTTPserver
II/OCompletionPortAPI(IOCP)/Thenon-blockingI/OengineofNode.js–libuvimplementation/Statefulmodulesimplementationtechniques,Decoratorpattern
composing,using/Compositionobjectaugmentation/ObjectaugmentationLevelUPdatabase,decorating/DecoratingaLevelUPdatabase
implementationtechniques,Proxypattern
objectcomposition/Objectcompositionobjectaugmentation(monkeypatching)/Objectaugmentation
inconsistentRead()function
about/UnleashingZalgo
IndexedDB/IntroducingLevelUPandLevelDB
informationhiding/ThemostcommondependencyinNode.jsinjectedservicelocator
about/Servicelocator
integrationpatterns,microservicearchitecture
about/IntegrationpatternsinaMicroservicearchitectureAPIproxy/TheAPIproxyAPIorchestration/APIorchestrationmessagebroker/Integrationwithamessagebroker
InterceptingFilterpattern
about/MiddlewareasapatternURL/Middlewareasapattern
interface/Statefulmodulesinterleavingpattern
considerations/Considerationsontheinterleavingpattern
interleavingpattern,withsetImmediate
about/InterleavingwithsetImmediatestepsofsubsetsumalgorithm,interleaving/Interleavingthestepsofthesubsetsumalgorithm
InversionofControl(IoC)
about/Plugin-controlledvsApplication-controlledextensionURL/Plugin-controlledvsApplication-controlledextension
isomorphic
about/Usingtheisomorphicmodels
Jjson-over-tcplibrary
URL/Implementingabasicfail-safesocket
JSONmessages
supporting,withmiddleware/AmiddlewaretosupportJSONmessages
JSONWebToken(JWT)
about/BuildinganauthenticationserverusinghardcodeddependenciesURL/Buildinganauthenticationserverusinghardcodeddependencies
jugglingdb
URL/Inthewild
KKeepItSimple,Stupid(KISS)principle/Simplicityandpragmatismkernelpanic/MonolithicarchitectureKISSprinciple/Thedifficultiesofasynchronousprogrammingkoa
referencelink/Generator-basedcontrolflowusingco
LLeastRecentlyUsed(LRU)algorithm/Notesaboutimplementingcachingmechanismslevel-filesystem
URL/Inthewild
level-inverted-index
URL/Inthewild
level-plus
URL/Inthewild
level.js
URL/UsingLevelUPthroughthefilesystemAPI
LevelDB
about/IntroducingLevelUPandLevelDB
levelgraph
URL/IntroducingLevelUPandLevelDB
levelofindirection
about/Modulesanddependencies
levelpackage
URL/ImplementingaLevelUPplugin
levelup
URL/Buildinganauthenticationserverusinghardcodeddependencies
LevelUPdatabase
decorating/DecoratingaLevelUPdatabaseURL/IntroducingLevelUPandLevelDBabout/IntroducingLevelUPandLevelDB
LevelUPecosystem
URL/IntroducingLevelUPandLevelDB
LevelUPplugin
implementing/ImplementingaLevelUPplugin
libuv
about/Thenon-blockingI/OengineofNode.js–libuvreferencelink/Thenon-blockingI/OengineofNode.js–libuv
limitedparallelexecution,asynclibrary
about/Limitedparallelexecution
limitedparallelexecution,generators
about/Limitedparallelexecutionoptions/Limitedparallelexecutionproducer-consumerpattern/Producer-consumerpatterndownloadtasksconcurrency,limiting/Limitingthedownloadtasks
concurrency
limitedparallelexecution,plainJavaScript
about/Limitedparallelexecutionconcurrency,limiting/Limitingtheconcurrencyconcurrency,limitingglobally/Globallylimitingtheconcurrencyqueues/Queuestotherescuewebspiderversion4/Webspiderversion4
limitedparallelexecution,promise
about/Limitedparallelexecution
loadbalancer/Scalingwithareverseproxyloadbalancing/Cloningandloadbalancing,Notesonthebehavioroftheclustermoduleloaddistribution/ThethreedimensionsofscalabilityloggingWritablestream
creating/CreatingaloggingWritablestream
logoutplugin
implementing/Implementingalogoutplugin
lookup/Thethreedimensionsofscalability
MMainfunction
about/Parallelexecution
makeGenerator()function/Thebasicsmasterprocess/Theclustermodulemeld
URL/Proxyintheecosystem–functionhooksandAOP
Memcached
URL/Notesaboutimplementingcachingmechanisms,Sharingthestateacrossmultipleinstances
memoization
about/Cachingrequestsinthetotalsaleswebserver
memoizeepackage
URL/Cachingrequestsinthetotalsaleswebserver
merge-streammodule
URL/Creatingatarballfrommultipledirectories
messagebroker
integration/Integrationwithamessagebrokerabout/Integrationwithamessagebroker
MessageBroker
about/Peer-to-peerorbroker-basedmessaging
messagequeue
about/Asynchronousmessagingandqueues
messagequeue(MQ)
about/Durablesubscribers
MessageQueueTelemetryTransport(MQTT)
about/Peer-to-peerorbroker-basedmessagingURL/Peer-to-peerorbroker-basedmessaging
messages
about/Messagetypes
messages,types
CommandMessage/MessagetypesEventMessage/MessagetypesDocumentMessage/Messagetypes
messagingsystem
fundamentals/Fundamentalsofamessagingsystemone-waypattern/One-wayandrequest/replypatternsrequest/replypattern/One-wayandrequest/replypatternsasynchronousmessaging/Asynchronousmessagingandqueuesqueues/Asynchronousmessagingandqueuespeer-to-peermessaging/Peer-to-peerorbroker-basedmessaging
Meteor
URL/Runningtheapplication
methods,Promiseinstance
promise.then(onFulfilled,onRejected)/Promises/A+implementationspromise.catch(onRejected)method/Promises/A+implementations
microservicearchitecture
about/TheMicroservicearchitectureloosecoupling/TheMicroservicearchitecturehighcohesion/TheMicroservicearchitectureintegrationcomplexity/TheMicroservicearchitectureexample/AnexampleoftheMicroservicearchitecturedataownership/AnexampleoftheMicroservicearchitectureintegrationpatterns/IntegrationpatternsinaMicroservicearchitecture
microservices
about/Thethreedimensionsofscalabilitypros/Prosandconsofmicroservices,Reusabilityacrossplatformsandlanguagescons/Prosandconsofmicroservices,AwaytoscaletheapplicationURL/Prosandconsofmicroservicesexpendableservice/Everyserviceisexpendableplatformreusability/Reusabilityacrossplatformsandlanguagesapplication,scaling/Awaytoscaletheapplicationchallenges/Thechallengesofmicroservices
Middleware
about/MiddlewareinExpress/MiddlewareinExpressaspattern/Middlewareasapatternusing,aspattern/Middlewareasapattern
middlewareframework,forØMQ/Creatingamiddlewareframeworkfor ØMQ
middleware
about/Proxyintheecosystem–functionhooksandAOP,Asynchronousmessagingandqueues
Middleware,usingaspattern
about/Middlewareasapatternsteps/Middlewareasapattern
middlewareframework
creating,forØMQ/Creatingamiddlewareframeworkfor ØMQMiddlewareManager/TheMiddlewareManager
middlewareframework,creatingforØMQ
MiddlewareManager/TheMiddlewareManagerjsonmiddleware/AmiddlewaretosupportJSONmessagesØMQmiddlewareframework,using/UsingtheØMQmiddlewareframework
MiddlewareManager
about/Middlewareasapattern
minification/DeclaringasetofdependenciestoaDIcontainerminificators/DeclaringasetofdependenciestoaDIcontainerminimistpackage
URL/UsingtheØMQPUB/SUBsockets
Mkdirpdependency
about/Creatingasimplewebspider
models
about/SharingbusinesslogicanddatavalidationusingBackboneModels
module
about/Smallmoduleswrapping,withpreinitializationqueues/Wrappingthemodulewithpreinitializationqueues
moduledefinitionpatterns
about/Moduledefinitionpatternsnamedexports/Namedexportsfunction,exporting/Exportingafunctionconstructor,exporting/Exportingaconstructorinstance,exporting/Exportinganinstancemodules,modifyingonglobalscope/Modifyingothermodulesortheglobalscope
moduleimpersonationpattern/Usinghardcodeddependenciesmodules
about/Modulesanddependenciesproperties/ThemostcommondependencyinNode.jssharing/Sharingmodules
modulesystem
about/Themodulesystemanditspatternspatterns/Themodulesystemanditspatternsrevealingmodulepattern/Therevealingmodulepattern
modulewiringpatterns
about/Patternsforwiringmoduleshardcodeddependency/Hardcodeddependencydependencyinjection(DI)/Dependencyinjectionservicelocator/Servicelocatordependencyinjectioncontainer/Dependencyinjectioncontainer
MongoDB
URL/Sharingthestateacrossmultipleinstances
Mongoose
about/InthewildURL/Inthewildreferences/Inthewild
Monit
URL/LoadbalancingwithNginx
monkeypatching
about/Modifyingothermodulesortheglobalscope
monkeypatching(Objectaugmentation)/Objectaugmentationmonolithicapplications
about/Thethreedimensionsofscalability
monolithicarchitecture
about/Monolithicarchitecture
monolithickernels/Monolithicarchitecturemonotonictimestamp
using/ImplementingareliablehistoryserviceusingAMQPURL/ImplementingareliablehistoryserviceusingAMQP
multipipepackage
URL/Combiningstreams
multipledirectories
tarball,creatingfrom/Creatingatarballfrommultipledirectories
multiplexer(mux)/Multiplexinganddemultiplexingmultiplexing
about/Multiplexinganddemultiplexingclientsideapplication,creating/Clientside–Multiplexing
multiprocesspattern
considerations/Considerationsonthemultiprocesspattern
multistream-mergemodule
URL/Creatingatarballfrommultipledirectories
multistreampackage
URL/Creatingatarballfrommultipledirectories
Mustache
URL/CreatinganUMDmoduleabout/CreatinganUMDmodule
mux/demuxapplication
running/Runningthemux/demuxapplication
ØMQ
middlewareframework,creatingfor/Creatingamiddlewareframeworkfor ØMQURL/UsingtheØMQmiddlewareframework,Peer-to-peerloadbalancing,Peer-to-peerorbroker-basedmessaging,IntroducingØMQabout/IntroducingØMQURL,forinstallation/Designingapeer-to-peerarchitectureforthechatserver
ØMQfan-out/fan-inpattern
about/TheØMQfan-out/fan-inpatternPUSH/PULLsockets/PUSH/PULLsocketsdistributedhashsumcracker,building/BuildingadistributedhashsumcrackerwithØMQ
ØMQmiddlewareframework
using/UsingtheØMQmiddlewareframeworkserver/Theserverclient/Theclient
ØMQPUB/SUBsockets
using/UsingtheØMQPUB/SUBsockets
Nnamedexports
about/Namedexports
namemangling/DeclaringasetofdependenciestoaDIcontainerNginx
URL/Scalingwithareverseproxy,LoadbalancingwithNginxinstalling,URL/LoadbalancingwithNginxloadbalancing/LoadbalancingwithNginx
node-core
about/TherecipeforNode.js
node-globmodule
referencelink/CombinecallbacksandEventEmitter
node-uuidpackage
using/AbstractingtherequestURL/Abstractingtherequest
Node.js
URL,forblog/Anatomyofstreams
Node.js-basedproxies/ScalingwithareverseproxyNode.jsapplications
scaling/ScalingNode.jsapplications
Node.jscallbackconventions
about/Node.jscallbackconventionscallbacks,passingaslastargument/Callbackscomelasterror,passingasfirstargument/Errorcomesfirsterrorpropagation/Propagatingerrorsuncaughtexceptions/Uncaughtexceptions
Node.jsmodules
about/Node.jsmodulesexplainedhomemademoduleloader/Ahomemademoduleloaderdefining/Definingamoduleglobals,defining/Definingglobalsmodule.exports,versusexports/module.exportsvsexportsrequirefunction/requireissynchronous
resolvingalgorithm/Theresolvingalgorithmmodulecache/Themodulecachecycles/Cycles
Node.jsphilosophy
about/TheNode.jsphilosophysimplicity/Simplicityandpragmatismpragmatism/Simplicityandpragmatism
Node.jsstylefunction
promisifying/PromisifyingaNode.jsstylefunction
nodebb
URL/Pluginsaspackages
nodebb-plugin-mentions
URL/Usinghardcodeddependencies
nodebb-plugin-poll
URL/Usinghardcodeddependencies
non-flowingmode,readablestreams/Thenon-flowingmodenormalfunction
about/Generators
npmdependencies
request/CreatingasimplewebspiderMkdirp/Creatingasimplewebspider
nscale
URL/Thechallengesofmicroservices
OObject-DocumentMapping(ODM)/Inthewildobject-pathlibrary
URL/Multi-formatconfigurationobjects
Object.defineProperty()
URL/Acomparisonofthedifferenttechniques
objectstreams
multiplexing/Multiplexinganddemultiplexingobjectstreamsdemultiplexing/Multiplexinganddemultiplexingobjectstreams
Observerpattern
about/Publish/subscribepattern
observerpattern
about/TheobserverpatternEventEmitter/TheEventEmitterEventEmitter,using/CreateanduseanEventEmitterEventEmitter,creating/CreateanduseanEventEmittererrors,propagating/Propagatingerrorsobservableobject,creating/Makeanyobjectobservablesynchronousevents/Synchronousandasynchronouseventsasynchronousevents/SynchronousandasynchronouseventsEventEmitter,versuscallbacks/EventEmittervsCallbackscallbacks,combiningwithEventEmitter/CombinecallbacksandEventEmitter
one-waypattern
about/One-wayandrequest/replypatterns
operatingmodes,streams
binary/Anatomyofstreamsobject/Anatomyofstreams
OperationalTransformation(OT)/Command
URL/Command
options,stream.Writable
highWaterMark/ImplementingWritablestreams
decodeStrings/ImplementingWritablestreams
orderedparallelexecution,streams/Orderedparallelexecutionorganization,Commandpattern
Command/CommandClient/CommandInvoker/CommandTarget(orReceiver)/Command
Ppackages
about/Smallmodules
packages,streams
about/Usefulpackagesforworkingwithstreamsreadable-stream/readable-streamthrough/throughandfromfrom/throughandfrom
packetswitching
about/Buildingaremotelogger
parallelexecution,asynclibrary
about/Parallelexecution
parallelexecution,generators
about/Parallelexecution
parallelexecution,plainJavaScript
about/Parallelexecutionwebspiderversion3/Webspiderversion3pattern/Thepatternraceconditions,fixing/Fixingraceconditionsinthepresenceofconcurrenttasks
parallelexecution,promise
about/Parallelexecution
parallelpipeline
about/PipelinesandtaskdistributionpatternsØMQfan-out/fan-inpattern/TheØMQfan-out/fan-inpattern
Passport.js
URL/Inthewild
peer-to-peerloadbalancing
about/Peer-to-peerloadbalancingHTTPclient,implementingformultipleserverrequestbalancing/ImplementinganHTTPclientthatcanbalancerequestsacrossmultipleservers
peer-to-peermessaging
about/Peer-to-peerorbroker-basedmessaging
peer-to-peerpublish/subscribepattern
withØMQ/Peer-to-peerpublish/subscribewithØMQarchitecture,designing/Designingapeer-to-peerarchitectureforthechatserverØMQPUB/SUBsockets,using/UsingtheØMQPUB/SUBsockets
pipe()method/Composabilitypipeline/Middlewareasapatternpipelines
about/Pipelinesandtaskdistributionpatternscreating/Pipelinesandtaskdistributionpatternscompetingconsumers/PipelinesandcompetingconsumersinAMQPpoint-to-pointcommunications/Point-to-pointcommunicationsandcompetingconsumers
pipes
used,forconnectingstreams/Connectingstreamsusingpipes
pipingpatterns
about/Pipingpatterns
plainJavaScript,using
about/UsingplainJavaScriptcallbackdiscipline/Callbackdisciplinecallbackdiscipline,applying/Applyingthecallbackdisciplinesequentialexecution/Sequentialexecutionparallelexecution/Parallelexecutionlimitedparallelexecution/Limitedparallelexecutionpros/Comparisoncons/Comparison
Plugin-controlled
versusApplication-controlledextension/Plugin-controlledvsApplication-controlledextension
plugininfrastructure/Plugin-controlledvsApplication-controlledextensionplugins
about/Wiringplugins
plugins,aspackages
example/Pluginsaspackages
pm2
URL/Zero-downtimerestart,LoadbalancingwithNginx
point-to-pointcommunications
about/Point-to-pointcommunicationsandcompetingconsumershashsumcracker,implementing/ImplementingthehashsumcrackerusingAMQP
PostgreSQL
URL/Sharingthestateacrossmultipleinstances
PouchDB
URL/IntroducingLevelUPandLevelDB
preinitializationqueues
about/Preinitializationqueuesasynchronouslyinitializedmodules,implementing/Implementingamodulethatinitializesasynchronouslymodule,wrappingwith/Wrappingthemodulewithpreinitializationqueues
process.nextTick()
about/Deferredexecution
process.nextTick()function/Considerationsontheinterleavingpatternproducer-consumerpattern/Limitedparallelexecutionpromise
about/Promises,Whatisapromise?working/Whatisapromise?Promises/A+implementations/Promises/A+implementationsNode.jsstylefunction,promisifying/PromisifyingaNode.jsstylefunctionsequentialexecution/Sequentialexecutionparallelexecution/Parallelexecutionlimitedparallelexecution/Limitedparallelexecutionpros/Comparisoncons/Comparison
Promise.all(array)method
about/Promises/A+implementations
promise.catch(onRejected)method
about/Promises/A+implementations
Promise.reject(err)method
about/Promises/A+implementations
Promise.resolve(obj)method
about/Promises/A+implementations
promise.then(onFulfilled,onRejected)method
about/Promises/A+implementations
Promises
dealing,withcaching/BatchingandcachingwithPromisesdealing,withbatching/BatchingandcachingwithPromises
Promises/A+specification
URL/Promises,Whatisapromise?implementing/Promises/A+implementations
promisification
about/PromisifyingaNode.jsstylefunction
promisifiedversion
about/PromisifyingaNode.jsstylefunction
propertyinjection
about/Thedifferenttypesofdependencyinjection
Proxypattern
about/Proxyimplementing,techniques/Techniquesforimplementingproxiestechniques,comparing/AcomparisonofthedifferenttechniquesloggingWritablestream,creating/CreatingaloggingWritablestreamfunctionhooking/Proxyintheecosystem–functionhooksandAOPAspectOrientedProgramming(AOP)/Proxyintheecosystem–functionhooksandAOP
pseudo-classicalinheritance/Objectcompositionpublish/subscribepattern/Integrationwithamessagebroker
about/Publish/subscribepatternreal-timechatapplication,building/Buildingaminimalistreal-timechatapplicationRedis,using/UsingRedisasamessagebrokerpeer-to-peerpublish/subscribe,withØMQ/Peer-to-peerpublish/subscribewithØMQdurablesubscribers/Durablesubscribers
publisher
about/Publish/subscribepattern
QQlibrary
URL/Promises/A+implementationsreferencelink/Promises/A+implementations
QualityofService(QoS)
about/DurablesubscribersQoS0,atmostonce/DurablesubscribersQoS1,atleastonce/DurablesubscribersQoS2,exactlyonce/Durablesubscribers
queue,AMQP
about/IntroducingAMQPdurable/IntroducingAMQPexclusive/IntroducingAMQPauto-delete/IntroducingAMQP
queues
about/Queuestotherescue,Asynchronousmessagingandqueues
RRabbitMQ
URL/Peer-to-peerorbroker-basedmessaging,Designingahistoryserviceforthechatapplicationusing/DurablesubscriberswithAMQPandRabbitMQURL,forinstallation/Designingahistoryserviceforthechatapplication
React
URL/Runningtheapplication
reactorpattern
core/Smallcoremodules/Smallmodules
surfacearea/Smallsurfaceareaabout/Thereactorpattern,ThereactorpatternI/O/I/OisslowblockingI/O/BlockingI/Onon-blockingI/O/Non-blockingI/Oeventdemultiplexing/Eventdemultiplexingstructure/ThereactorpatternEventDemultiplexer/ThereactorpatternEventQueue/Thereactorpatternlibuv/Thenon-blockingI/OengineofNode.js–libuvcomponents/TherecipeforNode.jsrecipe,forNode.js/TherecipeforNode.js
read()method/Thenon-flowingmodereadable-streampackage
about/readable-streamURL/readable-stream
Readablestream
implementing/ImplementingReadablestreams
readablestreams
about/Readablestreamsnon-flowingmode/Thenon-flowingmodeflowingmode/Theflowingmode
real-timechatapplication
building/Buildingaminimalistreal-timechatapplicationserverside,implementing/Implementingtheserversideclientside,implementing/Implementingtheclientsidescaling/Runningandscalingthechatapplicationexecuting/Runningandscalingthechatapplicationhistoryservice,designing/Designingahistoryserviceforthechatapplicationhistoryservice,implementingwithAMQP/ImplementingareliablehistoryserviceusingAMQPintegrating,withAMQP/IntegratingthechatapplicationwithAMQP
realsingleton
about/Servicelocator
Redis
URL/Notesaboutimplementingcachingmechanisms,Sharingthestateacrossmultipleinstances,UsingRedisasamessagebrokerabout/UsingRedisasamessagebrokerusing,asmessagebroker/UsingRedisasamessagebrokerURL,forinstallation/UsingRedisasamessagebroker
redispackage
URL/UsingRedisasamessagebrokerabout/UsingRedisasamessagebroker
remotelogger
building/Buildingaremotelogger
RemoteProcedureCall(RPC)
about/Messagetypes
Rendr
URL/Runningtheapplication
request/replyabstraction
implementing,correlationidentifierused/Implementingarequest/replyabstractionusingcorrelationidentifiersrequest,abstracting/Abstractingtherequestreply,abstracting/Abstractingthereplyreplier,creating/Tryingthefullrequest/replycycle
request/replypattern
about/One-wayandrequest/replypatterns,Request/replypatternscorrelationidentifier/Correlationidentifierreturnaddress/Returnaddressreferencelink/Implementingtherequestorandthereplier
requestdependency
about/Creatingasimplewebspider
require()function
about/Ahomemademoduleloader
RequireJS
URL/UniversalModuleDefinition
resiliency,clustermodule/Resiliencyandavailabilitywiththeclustermoduleresolvingalgorithm
about/Theresolvingalgorithmfilemodules/Theresolvingalgorithmcoremodules/Theresolvingalgorithmpackagemodules/Theresolvingalgorithmreferencelink/Theresolvingalgorithm
Restify
URL/Inthewild
returnaddress
about/Returnaddressimplementing,inAMQP/ImplementingthereturnaddresspatterninAMQPrequestabstraction,implementing/Implementingtherequestabstractionreplyabstraction,implementing/Implementingthereplyabstractionrequestor,implementing/Implementingtherequestorandthereplierreplier,implementing/Implementingtherequestorandthereplier
revealingmodulepattern
about/Therevealingmodulepattern
RevealingModulepattern/CreatinganUMDmodulereverseproxy
scaling/Scalingwithareverseproxyabout/ScalingwithareverseproxyURL/Scalingwithareverseproxyloadbalancing,withNginx/LoadbalancingwithNginx
roundrobin/NotesonthebehavioroftheclustermoduleRSVP
URL/Promises/A+implementations
Sscalability
about/Anintroductiontoapplicationscalingdimensions/Thethreedimensionsofscalability
scalecube/Thethreedimensionsofscalabilityseaport
URL/Implementingadynamicloadbalancerwithhttp-proxyandseaport
semi-coroutines
about/Generators
Seneca
URL/Thechallengesofmicroservices
sequentialexecution,asynchronouscontrolflowofstreams
about/Sequentialexecution
sequentialexecution,asynclibrary
about/Sequentialexecutionfunctions/Sequentialexecutionknowntasks,executinginsequence/Sequentialexecutionofaknownsetoftaskssequentialiteration/Sequentialiteration
sequentialexecution,generators
about/Sequentialexecution
sequentialexecution,plainJavaScript
about/Sequentialexecutionanalyzing/Sequentialexecutionvariations/Sequentialexecutionknowntasks,executinginsequence/Executingaknownsetoftasksin
sequencesequentialiteration/Sequentialiterationwebspiderversion2,sequentialiteration/Webspiderversion2sequentialcrawlingoflinks,sequentialiteration/Sequentialcrawlingoflinkspattern,sequentialiteration/Thepattern
sequentialexecution,promise
about/Sequentialexecutionsequentialiteration/Sequentialiterationpattern,sequentialiteration/Sequentialiteration–thepattern
server
implementing,withoutcaching/Implementingaserverwithnocachingorbatchingimplementing,withoutbatching/Implementingaserverwithnocachingorbatching
servicelocator
about/Servicelocatorpros/Prosandconsofaservicelocatorcons/Prosandconsofaservicelocatorreusabilityapproach/Prosandconsofaservicelocatorreadabilityapproach/Prosandconsofaservicelocatorused,forexposingservices/Exposingservicesusingaservicelocator
serviceLocatormodule
factory()method/Refactoringtheauthenticationservertouseaservicelocatorregister()method/Refactoringtheauthenticationservertouseaservicelocatorget()method/Refactoringtheauthenticationservertouseaservicelocator
ServiceRegistry
using/UsingaServiceRegistrydynamicloadbalancer,implementingwithhttp-proxy/Implementingadynamicloadbalancerwithhttp-proxyandseaportdynamicloadbalancer,implementingwithseaport/Implementingadynamicloadbalancerwithhttp-proxyandseaport
services
exposing,servicelocatorused/Exposingservicesusingaservicelocatorexposing,dependencyinjectionused/Exposingservicesusingdependencyinjectionexposing,dependencyinjectioncontainerused/Exposingservicesusingadependencyinjectioncontainer
setandforgetparadigm
about/Durablesubscribers
sharding/Thethreedimensionsofscalabilitysharedrendering
about/Runningtheapplication
siege
URL/BuildingasimpleHTTPserver
Simple/StreamingTextOrientatedMessagingProtocol(STOMP)
about/Peer-to-peerorbroker-basedmessagingURL/Peer-to-peerorbroker-basedmessaging
SinglePageApplications/BuildinganauthenticationserverusinghardcodeddependenciesSingleton/ExportinganinstanceSingletonpattern
about/Statefulmodules,TheSingletonpatterninNode.js
sink
about/Pipelinesandtaskdistributionpatterns
SLA(ServiceLevelAgreement)/Zero-downtimerestartSocket.io
URL/Stickyloadbalancing
softwaredevelopmentphilosophies
referencelink/TheNode.jsphilosophy
spatialefficiencyapproach,streams
about/Spatialefficiencygzipping,withbufferedAPI/GzippingusingabufferedAPIgzipping,withstreams/Gzippingusingstreams
splitpackage
URL/ImplementingaURLstatusmonitoringapplication
SSLterminationpoint/Scalingwithareverseproxystandaloneinstances/Scalingwithareverseproxystatefulcommunications
dealingwith/Dealingwithstatefulcommunicationsstate,sharingacrossmultipleinstances/Sharingthestateacrossmultipleinstancesstickyloadbalancing/Stickyloadbalancing
statefulmodules
about/StatefulmodulesSingletonpattern/TheSingletonpatterninNode.js
Statepattern
about/Statefail-safesocket,implementing/Implementingabasicfail-safesocket
staticmethods,Promiseobject
Promise.resolve(obj)/Promises/A+implementationsPromise.reject(err)/Promises/A+implementationsPromise.all(array)/Promises/A+implementations
sticky-session
URL/Stickyloadbalancing
stickyloadbalancing/StickyloadbalancingStorefront-end/APIorchestrationStrategypattern
about/Strategymultiformatconfigurationobject/Multi-formatconfigurationobjects
Passport.js/Inthewild
Streamclass
URL,forAPIs/Writingtoastream
streaming
versusbuffering/BufferingvsStreaming
Streamline
referencelink/Comparison
streams
needfor/Discoveringtheimportanceofstreamsspatialefficiencyapproach/Spatialefficiencytimeefficiencyapproach/Timeefficiencycomposabilityapproach/Composabilityabout/Gettingstartedwithstreamsanatomy/Anatomyofstreamsreadingfrom/Readingfromastreamwritingto/Writingtoastreamconnecting,pipesused/Connectingstreamsusingpipespackages/Usefulpackagesforworkingwithstreamsasynchronouscontrolflow/Asynchronouscontrolflowwithstreamscombining/Combiningstreamsmerging/Mergingstreams
streams,forking
about/Forkingstreamsmultiplechecksumgenerator,implementing/Implementingamultiplechecksumgenerator
Streams1/readable-streamStreams2/readable-streamStreams3/readable-streamsubject/Proxysublevelplugin
URL/Thedbmodule
subscribers
about/Publish/subscribepattern
subsetsumalgorithm
steps,interleavingof/Interleavingthestepsofthesubsetsumalgorithm
subsetsumproblem
solving/Solvingthesubsetsumproblem
subsetsumtask,delegating
about/Delegatingthesubsetsumtasktootherprocessesprocesspool,implementing/Implementingaprocesspoolchildprocesscommunication/Communicatingwithachildprocessparentprocesscommunication/Communicatingwiththeparentprocess
supervisor/LoadbalancingwithNginxsuspend
referencelink/Generator-basedcontrolflowusingco
synchronouseventdemultiplexer
about/Eventdemultiplexing
synchronousfunction
about/Asynchronouscontinuation-passingstyle
Systemd/LoadbalancingwithNginx
Ttarball
creating,frommultipledirectories/Creatingatarballfrommultipledirectories
tarpackage
URL/Creatingatarballfrommultipledirectories
taskdistributionpattern
about/Pipelinesandtaskdistributionpatterns
taskpattern
about/Thetaskpattern
TaskQueueclass/Queuestotherescuetechnicaldebt
about/Modulesanddependencies
templatemethods/TemplateTemplatepattern
about/Templateconfigurationmanagertemplate/Aconfigurationmanagertemplate
ternary-streampackage
URL/Multiplexinganddemultiplexingobjectstreams
thenable
about/Whatisapromise?
threads
about/Considerationsonthemultiprocesspattern
through2package
URL/throughandfrom
through2-parallelpackage
URL/Orderedparallelexecution
throughpackage/throughandfrom
URL/throughandfrom
thunk
about/Asynchronouscontrolflowwithgenerators
thunkify
referencelink/Generator-basedcontrolflowusingco
timeefficiencyapproach,streams
about/Timeefficiency
tolerancetofailures/ScalingNode.jsapplicationstransforms,onproject'swikipage
URL/TheadvantagesofusingBrowserify
Transformstreams
about/Transformstreamsimplementing/ImplementingTransformstreams
UUMDmodule
creating/CreatinganUMDmodule
UMDpattern
considerations/ConsiderationsontheUMDpatternURL/ConsiderationsontheUMDpattern
unorderedlimitedparallelexecution,asynchronouscontrolflowofstreams
about/Unorderedlimitedparallelexecution
unorderedparallelexecution,asynchronouscontrolflowofstreams
about/Unorderedparallelexecutionunorderedparallelstream,implementing/ImplementinganunorderedparallelstreamURLstatusmonitoring,implementing/ImplementingaURLstatusmonitoringapplication
Upstart
URL/LoadbalancingwithNginx
usemultipleprocessors
about/Usingmultipleprocesses
userland
about/Smallcore
userspace
about/Smallcore
uses,Proxypattern
datavalidation/Proxysecurity/Proxycaching/Proxylazyinitialization/Proxylogging/Proxyremoteobjects/Proxy
VV8
referencelink/Thecallbackhell
variations-streamlibrary
using/ImplementingtheventilatorURL/Implementingtheventilator
ventilator
about/Pipelinesandtaskdistributionpatterns
verticalscaling/Cloningandloadbalancingvmmodule
referencelink/Ahomemademoduleloader
Vow
URL/Promises/A+implementations
W
watchify
URL/ExploringthemagicofBrowserify
WebSockets
about/RunningandscalingthechatapplicationURL/Runningandscalingthechatapplication
webspider/Creatingasimplewebspiderwebworker-threads
URL/Considerationsonthemultiprocesspattern
When.js
URL/Promises/A+implementations
wiringplugins
about/Wiringplugins
worker
about/Delegatingthesubsetsumtasktootherprocesses
Writablestream
implementing/ImplementingWritablestreams
writablestreams
about/Writablestreamsback-pressure/Back-pressure
wspackage
URL/Implementingtheserverside
Yyieldstatement
about/Generators
Zzero-downtimerestart/Zero-downtimerestartzmqpackage
URL/UsingtheØMQPUB/SUBsockets