1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068806980708071807280738074807580768077807880798080808180828083808480858086808780888089809080918092809380948095809680978098809981008101810281038104810581068107810881098110811181128113811481158116811781188119812081218122812381248125812681278128812981308131813281338134813581368137813881398140814181428143814481458146814781488149815081518152815381548155815681578158815981608161816281638164816581668167816881698170817181728173817481758176817781788179818081818182818381848185818681878188818981908191819281938194819581968197819881998200820182028203820482058206820782088209821082118212821382148215821682178218821982208221822282238224822582268227822882298230823182328233823482358236823782388239824082418242824382448245824682478248824982508251825282538254825582568257825882598260826182628263826482658266826782688269827082718272827382748275827682778278827982808281828282838284828582868287828882898290829182928293829482958296829782988299830083018302830383048305830683078308830983108311831283138314831583168317831883198320832183228323832483258326832783288329833083318332833383348335833683378338833983408341834283438344834583468347834883498350835183528353835483558356835783588359836083618362836383648365836683678368836983708371837283738374837583768377837883798380838183828383838483858386838783888389839083918392839383948395839683978398839984008401840284038404840584068407840884098410841184128413841484158416841784188419842084218422842384248425842684278428842984308431843284338434843584368437843884398440844184428443844484458446844784488449845084518452845384548455845684578458845984608461846284638464846584668467846884698470847184728473847484758476847784788479848084818482848384848485848684878488848984908491849284938494849584968497849884998500850185028503850485058506850785088509851085118512851385148515851685178518851985208521852285238524852585268527852885298530853185328533853485358536853785388539854085418542854385448545854685478548854985508551855285538554855585568557855885598560856185628563856485658566856785688569857085718572857385748575857685778578857985808581858285838584858585868587858885898590859185928593859485958596859785988599860086018602860386048605860686078608860986108611861286138614861586168617861886198620862186228623862486258626862786288629863086318632863386348635863686378638863986408641864286438644864586468647864886498650865186528653865486558656865786588659866086618662866386648665866686678668866986708671867286738674867586768677867886798680868186828683868486858686868786888689869086918692869386948695869686978698869987008701870287038704870587068707870887098710871187128713871487158716871787188719872087218722872387248725872687278728872987308731873287338734873587368737873887398740874187428743874487458746874787488749875087518752875387548755875687578758875987608761876287638764876587668767876887698770877187728773877487758776877787788779878087818782878387848785878687878788878987908791879287938794879587968797879887998800880188028803880488058806880788088809881088118812881388148815881688178818881988208821882288238824882588268827882888298830883188328833883488358836883788388839884088418842884388448845884688478848884988508851885288538854885588568857885888598860886188628863886488658866886788688869887088718872887388748875887688778878887988808881888288838884888588868887888888898890889188928893889488958896889788988899890089018902890389048905890689078908890989108911891289138914891589168917891889198920892189228923892489258926892789288929893089318932893389348935893689378938893989408941894289438944894589468947894889498950895189528953895489558956895789588959896089618962896389648965896689678968896989708971897289738974897589768977897889798980898189828983898489858986898789888989899089918992899389948995899689978998899990009001900290039004900590069007900890099010901190129013901490159016901790189019902090219022902390249025902690279028902990309031903290339034903590369037903890399040904190429043904490459046904790489049905090519052905390549055905690579058905990609061906290639064906590669067906890699070907190729073907490759076907790789079908090819082908390849085908690879088908990909091909290939094909590969097909890999100910191029103910491059106910791089109911091119112911391149115911691179118911991209121912291239124912591269127912891299130913191329133913491359136913791389139914091419142914391449145914691479148914991509151915291539154915591569157915891599160916191629163916491659166916791689169917091719172917391749175917691779178917991809181918291839184918591869187918891899190919191929193919491959196919791989199920092019202920392049205920692079208920992109211921292139214921592169217921892199220922192229223922492259226922792289229923092319232923392349235923692379238923992409241924292439244924592469247924892499250925192529253925492559256925792589259926092619262926392649265926692679268926992709271927292739274927592769277927892799280928192829283928492859286928792889289929092919292929392949295929692979298929993009301930293039304930593069307930893099310931193129313931493159316931793189319932093219322932393249325932693279328932993309331933293339334933593369337933893399340934193429343934493459346934793489349935093519352935393549355935693579358935993609361936293639364936593669367936893699370937193729373937493759376937793789379938093819382938393849385938693879388938993909391939293939394939593969397939893999400940194029403940494059406940794089409941094119412941394149415941694179418941994209421942294239424942594269427942894299430943194329433943494359436943794389439944094419442944394449445944694479448944994509451945294539454945594569457945894599460946194629463946494659466946794689469947094719472947394749475947694779478947994809481948294839484948594869487948894899490949194929493949494959496949794989499950095019502950395049505950695079508950995109511951295139514951595169517951895199520952195229523952495259526952795289529953095319532953395349535953695379538953995409541954295439544954595469547954895499550955195529553955495559556955795589559956095619562956395649565956695679568956995709571957295739574957595769577957895799580958195829583958495859586958795889589959095919592959395949595959695979598959996009601960296039604960596069607960896099610961196129613961496159616961796189619962096219622962396249625962696279628962996309631963296339634963596369637963896399640964196429643964496459646964796489649965096519652965396549655965696579658965996609661966296639664966596669667966896699670967196729673967496759676967796789679968096819682968396849685968696879688968996909691969296939694969596969697969896999700970197029703970497059706970797089709971097119712971397149715971697179718971997209721972297239724972597269727972897299730973197329733973497359736973797389739974097419742974397449745974697479748974997509751975297539754975597569757975897599760976197629763976497659766976797689769977097719772977397749775977697779778977997809781978297839784978597869787978897899790979197929793979497959796979797989799980098019802980398049805980698079808980998109811981298139814981598169817981898199820982198229823982498259826982798289829983098319832983398349835983698379838983998409841984298439844984598469847984898499850985198529853985498559856985798589859986098619862986398649865986698679868986998709871987298739874987598769877987898799880988198829883988498859886988798889889989098919892989398949895989698979898989999009901990299039904990599069907990899099910991199129913991499159916991799189919992099219922992399249925992699279928992999309931993299339934993599369937993899399940994199429943994499459946994799489949995099519952995399549955995699579958995999609961996299639964996599669967996899699970997199729973997499759976997799789979998099819982998399849985998699879988998999909991999299939994999599969997999899991000010001100021000310004100051000610007100081000910010100111001210013100141001510016100171001810019100201002110022100231002410025100261002710028100291003010031100321003310034100351003610037100381003910040100411004210043100441004510046100471004810049100501005110052100531005410055100561005710058100591006010061100621006310064100651006610067100681006910070100711007210073100741007510076100771007810079100801008110082100831008410085100861008710088100891009010091100921009310094100951009610097100981009910100101011010210103101041010510106101071010810109101101011110112101131011410115101161011710118101191012010121101221012310124101251012610127101281012910130101311013210133101341013510136101371013810139101401014110142101431014410145101461014710148101491015010151101521015310154101551015610157101581015910160101611016210163101641016510166101671016810169101701017110172101731017410175101761017710178101791018010181101821018310184101851018610187101881018910190101911019210193101941019510196101971019810199102001020110202102031020410205102061020710208102091021010211102121021310214102151021610217102181021910220102211022210223102241022510226102271022810229102301023110232102331023410235102361023710238102391024010241102421024310244102451024610247102481024910250102511025210253102541025510256102571025810259102601026110262102631026410265102661026710268102691027010271102721027310274102751027610277102781027910280102811028210283102841028510286102871028810289102901029110292102931029410295102961029710298102991030010301103021030310304103051030610307103081030910310103111031210313103141031510316103171031810319103201032110322103231032410325103261032710328103291033010331103321033310334103351033610337103381033910340103411034210343103441034510346103471034810349103501035110352103531035410355103561035710358103591036010361103621036310364103651036610367103681036910370103711037210373103741037510376103771037810379103801038110382103831038410385103861038710388103891039010391103921039310394103951039610397103981039910400104011040210403104041040510406104071040810409104101041110412104131041410415104161041710418104191042010421104221042310424104251042610427104281042910430104311043210433104341043510436104371043810439104401044110442104431044410445104461044710448104491045010451104521045310454104551045610457104581045910460104611046210463104641046510466104671046810469104701047110472104731047410475104761047710478104791048010481104821048310484104851048610487104881048910490104911049210493104941049510496104971049810499105001050110502105031050410505105061050710508105091051010511105121051310514105151051610517105181051910520105211052210523105241052510526105271052810529105301053110532105331053410535105361053710538105391054010541105421054310544105451054610547105481054910550105511055210553105541055510556105571055810559105601056110562105631056410565105661056710568105691057010571105721057310574105751057610577105781057910580105811058210583105841058510586105871058810589105901059110592105931059410595105961059710598105991060010601106021060310604106051060610607106081060910610106111061210613106141061510616106171061810619106201062110622106231062410625106261062710628106291063010631106321063310634106351063610637106381063910640106411064210643106441064510646106471064810649106501065110652106531065410655106561065710658106591066010661106621066310664106651066610667106681066910670106711067210673106741067510676106771067810679106801068110682106831068410685106861068710688106891069010691106921069310694106951069610697106981069910700107011070210703107041070510706107071070810709107101071110712107131071410715107161071710718107191072010721107221072310724107251072610727107281072910730107311073210733107341073510736107371073810739107401074110742107431074410745107461074710748107491075010751107521075310754107551075610757107581075910760107611076210763107641076510766107671076810769107701077110772107731077410775107761077710778107791078010781107821078310784107851078610787107881078910790107911079210793107941079510796107971079810799108001080110802108031080410805108061080710808108091081010811108121081310814108151081610817108181081910820108211082210823108241082510826108271082810829108301083110832108331083410835108361083710838108391084010841108421084310844108451084610847108481084910850108511085210853108541085510856108571085810859108601086110862108631086410865108661086710868108691087010871108721087310874108751087610877108781087910880108811088210883108841088510886108871088810889108901089110892108931089410895108961089710898108991090010901109021090310904109051090610907109081090910910109111091210913109141091510916109171091810919109201092110922109231092410925109261092710928109291093010931109321093310934109351093610937109381093910940109411094210943109441094510946109471094810949109501095110952109531095410955109561095710958109591096010961109621096310964109651096610967109681096910970109711097210973109741097510976109771097810979109801098110982109831098410985109861098710988109891099010991109921099310994109951099610997109981099911000110011100211003110041100511006110071100811009110101101111012110131101411015110161101711018110191102011021110221102311024110251102611027110281102911030110311103211033110341103511036110371103811039110401104111042110431104411045110461104711048110491105011051110521105311054110551105611057110581105911060110611106211063110641106511066110671106811069110701107111072110731107411075110761107711078110791108011081110821108311084110851108611087110881108911090110911109211093110941109511096110971109811099111001110111102111031110411105111061110711108111091111011111111121111311114111151111611117111181111911120111211112211123111241112511126111271112811129111301113111132111331113411135111361113711138111391114011141111421114311144111451114611147111481114911150111511115211153111541115511156111571115811159111601116111162111631116411165111661116711168111691117011171111721117311174111751117611177111781117911180111811118211183111841118511186111871118811189111901119111192111931119411195111961119711198111991120011201112021120311204112051120611207112081120911210112111121211213112141121511216112171121811219112201122111222112231122411225112261122711228112291123011231112321123311234112351123611237112381123911240112411124211243112441124511246112471124811249112501125111252112531125411255112561125711258112591126011261112621126311264112651126611267112681126911270112711127211273112741127511276112771127811279112801128111282112831128411285112861128711288112891129011291112921129311294112951129611297112981129911300113011130211303113041130511306113071130811309113101131111312113131131411315113161131711318113191132011321113221132311324113251132611327113281132911330113311133211333113341133511336113371133811339113401134111342113431134411345113461134711348113491135011351113521135311354113551135611357113581135911360113611136211363113641136511366113671136811369113701137111372113731137411375113761137711378113791138011381113821138311384113851138611387113881138911390113911139211393113941139511396113971139811399114001140111402114031140411405114061140711408114091141011411114121141311414114151141611417114181141911420114211142211423114241142511426114271142811429114301143111432114331143411435114361143711438114391144011441114421144311444114451144611447114481144911450114511145211453114541145511456114571145811459114601146111462114631146411465114661146711468114691147011471114721147311474114751147611477114781147911480114811148211483114841148511486114871148811489114901149111492114931149411495114961149711498114991150011501115021150311504115051150611507115081150911510115111151211513115141151511516115171151811519115201152111522115231152411525115261152711528115291153011531115321153311534115351153611537115381153911540115411154211543115441154511546115471154811549115501155111552115531155411555115561155711558115591156011561115621156311564115651156611567115681156911570115711157211573115741157511576115771157811579115801158111582115831158411585115861158711588115891159011591115921159311594115951159611597115981159911600116011160211603116041160511606116071160811609116101161111612116131161411615116161161711618116191162011621116221162311624116251162611627116281162911630116311163211633116341163511636116371163811639116401164111642116431164411645116461164711648116491165011651116521165311654116551165611657116581165911660116611166211663116641166511666116671166811669116701167111672116731167411675116761167711678116791168011681116821168311684116851168611687116881168911690116911169211693116941169511696116971169811699117001170111702117031170411705117061170711708117091171011711117121171311714117151171611717117181171911720117211172211723117241172511726117271172811729117301173111732117331173411735117361173711738117391174011741117421174311744117451174611747117481174911750117511175211753117541175511756117571175811759117601176111762117631176411765117661176711768117691177011771117721177311774117751177611777117781177911780117811178211783117841178511786117871178811789117901179111792117931179411795117961179711798117991180011801118021180311804118051180611807118081180911810118111181211813118141181511816118171181811819118201182111822118231182411825118261182711828118291183011831118321183311834118351183611837118381183911840118411184211843118441184511846118471184811849118501185111852118531185411855118561185711858118591186011861118621186311864118651186611867118681186911870118711187211873118741187511876118771187811879118801188111882118831188411885118861188711888118891189011891118921189311894118951189611897118981189911900119011190211903119041190511906119071190811909119101191111912119131191411915119161191711918119191192011921119221192311924119251192611927119281192911930119311193211933119341193511936119371193811939119401194111942119431194411945119461194711948119491195011951119521195311954119551195611957119581195911960119611196211963119641196511966119671196811969119701197111972119731197411975119761197711978119791198011981119821198311984119851198611987119881198911990119911199211993119941199511996119971199811999120001200112002120031200412005120061200712008120091201012011120121201312014120151201612017120181201912020120211202212023120241202512026120271202812029120301203112032120331203412035120361203712038120391204012041120421204312044120451204612047120481204912050120511205212053120541205512056120571205812059120601206112062120631206412065120661206712068120691207012071120721207312074120751207612077120781207912080120811208212083120841208512086120871208812089120901209112092120931209412095120961209712098120991210012101121021210312104121051210612107121081210912110121111211212113121141211512116121171211812119121201212112122121231212412125121261212712128121291213012131121321213312134121351213612137121381213912140121411214212143121441214512146121471214812149121501215112152121531215412155121561215712158121591216012161121621216312164121651216612167121681216912170121711217212173121741217512176121771217812179121801218112182121831218412185121861218712188121891219012191121921219312194121951219612197121981219912200122011220212203122041220512206122071220812209122101221112212122131221412215122161221712218122191222012221122221222312224122251222612227122281222912230122311223212233122341223512236122371223812239122401224112242122431224412245122461224712248122491225012251122521225312254122551225612257122581225912260122611226212263122641226512266122671226812269122701227112272122731227412275122761227712278122791228012281122821228312284122851228612287122881228912290122911229212293122941229512296122971229812299123001230112302123031230412305123061230712308123091231012311123121231312314123151231612317123181231912320123211232212323123241232512326123271232812329123301233112332123331233412335123361233712338123391234012341123421234312344123451234612347123481234912350123511235212353123541235512356123571235812359123601236112362123631236412365123661236712368123691237012371123721237312374123751237612377123781237912380123811238212383123841238512386123871238812389123901239112392123931239412395123961239712398123991240012401124021240312404124051240612407124081240912410124111241212413124141241512416124171241812419124201242112422124231242412425124261242712428124291243012431124321243312434124351243612437124381243912440124411244212443124441244512446124471244812449124501245112452124531245412455124561245712458124591246012461124621246312464124651246612467124681246912470124711247212473124741247512476124771247812479124801248112482124831248412485124861248712488124891249012491124921249312494124951249612497124981249912500125011250212503125041250512506125071250812509125101251112512125131251412515125161251712518125191252012521125221252312524125251252612527125281252912530125311253212533125341253512536125371253812539125401254112542125431254412545125461254712548125491255012551125521255312554125551255612557125581255912560125611256212563125641256512566125671256812569125701257112572125731257412575125761257712578125791258012581125821258312584125851258612587125881258912590125911259212593125941259512596125971259812599126001260112602126031260412605126061260712608126091261012611126121261312614126151261612617126181261912620126211262212623126241262512626126271262812629126301263112632126331263412635126361263712638126391264012641126421264312644126451264612647126481264912650126511265212653126541265512656126571265812659126601266112662126631266412665126661266712668126691267012671126721267312674126751267612677126781267912680126811268212683126841268512686126871268812689126901269112692126931269412695126961269712698126991270012701127021270312704127051270612707127081270912710127111271212713127141271512716127171271812719127201272112722127231272412725127261272712728127291273012731127321273312734127351273612737127381273912740127411274212743127441274512746127471274812749127501275112752127531275412755127561275712758127591276012761127621276312764127651276612767127681276912770127711277212773127741277512776127771277812779127801278112782127831278412785127861278712788127891279012791127921279312794127951279612797127981279912800128011280212803128041280512806128071280812809128101281112812128131281412815128161281712818128191282012821128221282312824128251282612827128281282912830128311283212833128341283512836128371283812839128401284112842128431284412845128461284712848128491285012851128521285312854128551285612857128581285912860128611286212863128641286512866128671286812869128701287112872128731287412875128761287712878128791288012881128821288312884128851288612887128881288912890128911289212893128941289512896128971289812899129001290112902129031290412905129061290712908129091291012911129121291312914129151291612917129181291912920129211292212923129241292512926129271292812929129301293112932129331293412935129361293712938129391294012941129421294312944129451294612947129481294912950129511295212953129541295512956129571295812959129601296112962129631296412965129661296712968129691297012971129721297312974129751297612977129781297912980129811298212983129841298512986129871298812989129901299112992129931299412995129961299712998129991300013001130021300313004130051300613007130081300913010130111301213013130141301513016130171301813019130201302113022130231302413025130261302713028130291303013031130321303313034130351303613037130381303913040130411304213043130441304513046130471304813049130501305113052130531305413055130561305713058130591306013061130621306313064130651306613067130681306913070130711307213073130741307513076130771307813079130801308113082130831308413085130861308713088130891309013091130921309313094130951309613097130981309913100131011310213103131041310513106131071310813109131101311113112131131311413115131161311713118131191312013121131221312313124131251312613127131281312913130131311313213133131341313513136131371313813139131401314113142131431314413145131461314713148131491315013151131521315313154131551315613157131581315913160131611316213163131641316513166131671316813169131701317113172131731317413175131761317713178131791318013181131821318313184131851318613187131881318913190131911319213193131941319513196131971319813199132001320113202132031320413205132061320713208132091321013211132121321313214132151321613217132181321913220132211322213223132241322513226132271322813229132301323113232132331323413235132361323713238132391324013241132421324313244132451324613247132481324913250132511325213253132541325513256132571325813259132601326113262132631326413265132661326713268132691327013271132721327313274132751327613277132781327913280132811328213283132841328513286132871328813289132901329113292132931329413295132961329713298132991330013301133021330313304133051330613307133081330913310133111331213313133141331513316133171331813319133201332113322133231332413325133261332713328133291333013331133321333313334133351333613337133381333913340133411334213343133441334513346133471334813349133501335113352133531335413355133561335713358133591336013361133621336313364133651336613367133681336913370133711337213373133741337513376133771337813379133801338113382133831338413385133861338713388133891339013391133921339313394133951339613397133981339913400134011340213403134041340513406134071340813409134101341113412134131341413415134161341713418134191342013421134221342313424134251342613427134281342913430134311343213433134341343513436134371343813439134401344113442134431344413445134461344713448134491345013451134521345313454134551345613457134581345913460134611346213463134641346513466134671346813469134701347113472134731347413475134761347713478134791348013481134821348313484134851348613487134881348913490134911349213493134941349513496134971349813499135001350113502135031350413505135061350713508135091351013511135121351313514135151351613517135181351913520135211352213523135241352513526135271352813529135301353113532135331353413535135361353713538135391354013541135421354313544135451354613547135481354913550135511355213553135541355513556135571355813559135601356113562135631356413565135661356713568135691357013571135721357313574135751357613577135781357913580135811358213583135841358513586135871358813589135901359113592135931359413595135961359713598135991360013601136021360313604136051360613607136081360913610136111361213613136141361513616136171361813619136201362113622136231362413625136261362713628136291363013631136321363313634136351363613637136381363913640136411364213643136441364513646136471364813649136501365113652136531365413655136561365713658136591366013661136621366313664136651366613667136681366913670136711367213673136741367513676136771367813679136801368113682136831368413685136861368713688136891369013691136921369313694136951369613697136981369913700137011370213703137041370513706137071370813709137101371113712137131371413715137161371713718137191372013721137221372313724137251372613727137281372913730137311373213733137341373513736137371373813739137401374113742137431374413745137461374713748137491375013751137521375313754137551375613757137581375913760137611376213763137641376513766137671376813769137701377113772137731377413775137761377713778137791378013781137821378313784137851378613787137881378913790137911379213793137941379513796137971379813799138001380113802138031380413805138061380713808138091381013811138121381313814138151381613817138181381913820138211382213823138241382513826138271382813829138301383113832138331383413835138361383713838138391384013841138421384313844138451384613847138481384913850138511385213853138541385513856138571385813859138601386113862138631386413865138661386713868138691387013871138721387313874138751387613877138781387913880138811388213883138841388513886138871388813889138901389113892138931389413895138961389713898138991390013901139021390313904139051390613907139081390913910139111391213913139141391513916139171391813919139201392113922139231392413925139261392713928139291393013931139321393313934139351393613937139381393913940139411394213943139441394513946139471394813949139501395113952139531395413955139561395713958139591396013961139621396313964139651396613967139681396913970139711397213973139741397513976139771397813979139801398113982139831398413985139861398713988139891399013991139921399313994139951399613997139981399914000140011400214003140041400514006140071400814009140101401114012140131401414015140161401714018140191402014021140221402314024140251402614027140281402914030140311403214033140341403514036140371403814039140401404114042140431404414045140461404714048140491405014051140521405314054140551405614057140581405914060140611406214063140641406514066140671406814069140701407114072140731407414075140761407714078140791408014081140821408314084140851408614087140881408914090140911409214093140941409514096140971409814099141001410114102141031410414105141061410714108141091411014111141121411314114141151411614117141181411914120141211412214123141241412514126141271412814129141301413114132141331413414135141361413714138141391414014141141421414314144141451414614147141481414914150141511415214153141541415514156141571415814159141601416114162141631416414165141661416714168141691417014171141721417314174141751417614177141781417914180141811418214183141841418514186141871418814189141901419114192141931419414195141961419714198141991420014201142021420314204142051420614207142081420914210142111421214213142141421514216142171421814219142201422114222142231422414225142261422714228142291423014231142321423314234142351423614237142381423914240142411424214243142441424514246142471424814249142501425114252142531425414255142561425714258142591426014261142621426314264142651426614267142681426914270142711427214273142741427514276142771427814279142801428114282142831428414285142861428714288142891429014291142921429314294142951429614297142981429914300143011430214303143041430514306143071430814309143101431114312143131431414315143161431714318143191432014321143221432314324143251432614327143281432914330143311433214333143341433514336143371433814339143401434114342143431434414345143461434714348143491435014351143521435314354143551435614357143581435914360143611436214363143641436514366143671436814369143701437114372143731437414375143761437714378143791438014381143821438314384143851438614387143881438914390143911439214393143941439514396143971439814399144001440114402144031440414405144061440714408144091441014411144121441314414144151441614417144181441914420144211442214423144241442514426144271442814429144301443114432144331443414435144361443714438144391444014441144421444314444144451444614447144481444914450144511445214453144541445514456144571445814459144601446114462144631446414465144661446714468144691447014471144721447314474144751447614477144781447914480144811448214483144841448514486144871448814489144901449114492144931449414495144961449714498144991450014501145021450314504145051450614507145081450914510145111451214513145141451514516145171451814519145201452114522145231452414525145261452714528145291453014531145321453314534145351453614537145381453914540145411454214543145441454514546145471454814549145501455114552145531455414555145561455714558145591456014561145621456314564145651456614567145681456914570145711457214573145741457514576145771457814579145801458114582145831458414585145861458714588145891459014591145921459314594145951459614597145981459914600146011460214603146041460514606146071460814609146101461114612146131461414615146161461714618146191462014621146221462314624146251462614627146281462914630146311463214633146341463514636146371463814639146401464114642146431464414645146461464714648146491465014651146521465314654146551465614657146581465914660146611466214663146641466514666146671466814669146701467114672146731467414675146761467714678146791468014681146821468314684146851468614687146881468914690146911469214693146941469514696146971469814699147001470114702147031470414705147061470714708147091471014711147121471314714147151471614717147181471914720147211472214723147241472514726147271472814729147301473114732147331473414735147361473714738147391474014741147421474314744147451474614747147481474914750147511475214753147541475514756147571475814759147601476114762147631476414765147661476714768147691477014771147721477314774147751477614777147781477914780147811478214783147841478514786147871478814789147901479114792147931479414795147961479714798147991480014801148021480314804148051480614807148081480914810148111481214813148141481514816148171481814819148201482114822148231482414825148261482714828148291483014831148321483314834148351483614837148381483914840148411484214843148441484514846148471484814849148501485114852148531485414855148561485714858148591486014861148621486314864148651486614867148681486914870148711487214873148741487514876148771487814879148801488114882148831488414885148861488714888148891489014891148921489314894148951489614897148981489914900149011490214903149041490514906149071490814909149101491114912149131491414915149161491714918149191492014921149221492314924149251492614927149281492914930149311493214933149341493514936149371493814939149401494114942149431494414945149461494714948149491495014951149521495314954149551495614957149581495914960149611496214963149641496514966149671496814969149701497114972149731497414975149761497714978149791498014981149821498314984149851498614987149881498914990149911499214993149941499514996149971499814999150001500115002150031500415005150061500715008150091501015011150121501315014150151501615017150181501915020150211502215023150241502515026150271502815029150301503115032150331503415035150361503715038150391504015041150421504315044150451504615047150481504915050150511505215053150541505515056150571505815059150601506115062150631506415065150661506715068150691507015071150721507315074150751507615077150781507915080150811508215083150841508515086150871508815089150901509115092150931509415095150961509715098150991510015101151021510315104151051510615107151081510915110151111511215113151141511515116151171511815119151201512115122151231512415125151261512715128151291513015131151321513315134151351513615137151381513915140151411514215143151441514515146151471514815149151501515115152151531515415155151561515715158151591516015161151621516315164151651516615167151681516915170151711517215173151741517515176151771517815179151801518115182151831518415185151861518715188151891519015191151921519315194151951519615197151981519915200152011520215203152041520515206152071520815209152101521115212152131521415215152161521715218152191522015221152221522315224152251522615227152281522915230152311523215233152341523515236152371523815239152401524115242152431524415245152461524715248152491525015251152521525315254152551525615257152581525915260152611526215263152641526515266152671526815269152701527115272152731527415275152761527715278152791528015281152821528315284152851528615287152881528915290152911529215293152941529515296152971529815299153001530115302153031530415305153061530715308153091531015311153121531315314153151531615317153181531915320153211532215323153241532515326153271532815329153301533115332153331533415335153361533715338153391534015341153421534315344153451534615347153481534915350153511535215353153541535515356153571535815359153601536115362153631536415365153661536715368153691537015371153721537315374153751537615377153781537915380153811538215383153841538515386153871538815389153901539115392153931539415395153961539715398153991540015401154021540315404154051540615407154081540915410154111541215413154141541515416154171541815419154201542115422154231542415425154261542715428154291543015431154321543315434154351543615437154381543915440154411544215443154441544515446154471544815449154501545115452154531545415455154561545715458154591546015461154621546315464154651546615467154681546915470154711547215473154741547515476154771547815479154801548115482154831548415485154861548715488154891549015491154921549315494154951549615497154981549915500155011550215503155041550515506155071550815509155101551115512155131551415515155161551715518155191552015521155221552315524155251552615527155281552915530155311553215533155341553515536155371553815539155401554115542155431554415545155461554715548155491555015551155521555315554155551555615557155581555915560155611556215563155641556515566155671556815569155701557115572155731557415575155761557715578155791558015581155821558315584155851558615587155881558915590155911559215593155941559515596155971559815599156001560115602156031560415605156061560715608156091561015611156121561315614156151561615617156181561915620156211562215623156241562515626156271562815629156301563115632156331563415635156361563715638156391564015641156421564315644156451564615647156481564915650156511565215653156541565515656156571565815659156601566115662156631566415665156661566715668156691567015671156721567315674156751567615677156781567915680156811568215683156841568515686156871568815689156901569115692156931569415695156961569715698156991570015701157021570315704157051570615707157081570915710157111571215713157141571515716157171571815719157201572115722157231572415725157261572715728157291573015731157321573315734157351573615737157381573915740157411574215743157441574515746157471574815749157501575115752157531575415755157561575715758157591576015761157621576315764157651576615767157681576915770157711577215773157741577515776157771577815779157801578115782157831578415785157861578715788157891579015791157921579315794157951579615797157981579915800158011580215803158041580515806158071580815809158101581115812158131581415815158161581715818158191582015821158221582315824158251582615827158281582915830158311583215833158341583515836158371583815839158401584115842158431584415845158461584715848158491585015851158521585315854158551585615857158581585915860158611586215863158641586515866158671586815869158701587115872158731587415875158761587715878158791588015881158821588315884158851588615887158881588915890158911589215893158941589515896158971589815899159001590115902159031590415905159061590715908159091591015911159121591315914159151591615917159181591915920159211592215923159241592515926159271592815929159301593115932159331593415935159361593715938159391594015941159421594315944159451594615947159481594915950159511595215953159541595515956159571595815959159601596115962159631596415965159661596715968159691597015971159721597315974159751597615977159781597915980159811598215983159841598515986159871598815989159901599115992159931599415995159961599715998159991600016001160021600316004160051600616007160081600916010160111601216013160141601516016160171601816019160201602116022160231602416025160261602716028160291603016031160321603316034160351603616037160381603916040160411604216043160441604516046160471604816049160501605116052160531605416055160561605716058160591606016061160621606316064160651606616067160681606916070160711607216073160741607516076160771607816079160801608116082160831608416085160861608716088160891609016091160921609316094160951609616097160981609916100161011610216103161041610516106161071610816109161101611116112161131611416115161161611716118161191612016121161221612316124161251612616127161281612916130161311613216133161341613516136161371613816139161401614116142161431614416145161461614716148161491615016151161521615316154161551615616157161581615916160161611616216163161641616516166161671616816169161701617116172161731617416175161761617716178161791618016181161821618316184161851618616187161881618916190161911619216193161941619516196161971619816199162001620116202162031620416205162061620716208162091621016211162121621316214162151621616217162181621916220162211622216223162241622516226162271622816229162301623116232162331623416235162361623716238162391624016241162421624316244162451624616247162481624916250162511625216253162541625516256162571625816259162601626116262162631626416265162661626716268162691627016271162721627316274162751627616277162781627916280162811628216283162841628516286162871628816289162901629116292162931629416295162961629716298162991630016301163021630316304163051630616307163081630916310163111631216313163141631516316163171631816319163201632116322163231632416325163261632716328163291633016331163321633316334163351633616337163381633916340163411634216343163441634516346163471634816349163501635116352163531635416355163561635716358163591636016361163621636316364163651636616367163681636916370163711637216373163741637516376163771637816379163801638116382163831638416385163861638716388163891639016391163921639316394163951639616397163981639916400164011640216403164041640516406164071640816409164101641116412164131641416415164161641716418164191642016421164221642316424164251642616427164281642916430164311643216433164341643516436164371643816439164401644116442164431644416445164461644716448164491645016451164521645316454164551645616457164581645916460164611646216463164641646516466164671646816469164701647116472164731647416475164761647716478164791648016481164821648316484164851648616487164881648916490164911649216493164941649516496164971649816499165001650116502165031650416505165061650716508165091651016511165121651316514165151651616517165181651916520165211652216523165241652516526165271652816529165301653116532165331653416535165361653716538165391654016541165421654316544165451654616547165481654916550165511655216553165541655516556165571655816559165601656116562165631656416565165661656716568165691657016571165721657316574165751657616577165781657916580165811658216583165841658516586165871658816589165901659116592165931659416595165961659716598165991660016601166021660316604166051660616607166081660916610166111661216613166141661516616166171661816619166201662116622166231662416625166261662716628166291663016631166321663316634166351663616637166381663916640166411664216643166441664516646166471664816649166501665116652166531665416655166561665716658166591666016661166621666316664166651666616667166681666916670166711667216673166741667516676166771667816679166801668116682166831668416685166861668716688166891669016691166921669316694166951669616697166981669916700167011670216703167041670516706167071670816709167101671116712167131671416715167161671716718167191672016721167221672316724167251672616727167281672916730167311673216733167341673516736167371673816739167401674116742167431674416745167461674716748167491675016751167521675316754167551675616757167581675916760167611676216763167641676516766167671676816769167701677116772167731677416775167761677716778167791678016781167821678316784167851678616787167881678916790167911679216793167941679516796167971679816799168001680116802168031680416805168061680716808168091681016811168121681316814168151681616817168181681916820168211682216823168241682516826168271682816829168301683116832168331683416835168361683716838168391684016841168421684316844168451684616847168481684916850168511685216853168541685516856168571685816859168601686116862168631686416865168661686716868168691687016871168721687316874168751687616877168781687916880168811688216883168841688516886168871688816889168901689116892168931689416895168961689716898168991690016901169021690316904169051690616907169081690916910169111691216913169141691516916169171691816919169201692116922169231692416925169261692716928169291693016931169321693316934169351693616937169381693916940169411694216943169441694516946169471694816949169501695116952169531695416955169561695716958169591696016961169621696316964169651696616967169681696916970169711697216973169741697516976169771697816979169801698116982169831698416985169861698716988169891699016991169921699316994169951699616997169981699917000170011700217003170041700517006170071700817009170101701117012170131701417015170161701717018170191702017021170221702317024170251702617027170281702917030170311703217033170341703517036170371703817039170401704117042170431704417045170461704717048170491705017051170521705317054170551705617057170581705917060170611706217063170641706517066170671706817069170701707117072170731707417075170761707717078170791708017081170821708317084170851708617087170881708917090170911709217093170941709517096170971709817099171001710117102171031710417105171061710717108171091711017111171121711317114171151711617117171181711917120171211712217123171241712517126171271712817129171301713117132171331713417135171361713717138171391714017141171421714317144171451714617147171481714917150171511715217153171541715517156171571715817159171601716117162171631716417165171661716717168171691717017171171721717317174171751717617177171781717917180171811718217183171841718517186171871718817189171901719117192171931719417195171961719717198171991720017201172021720317204172051720617207172081720917210172111721217213172141721517216172171721817219172201722117222172231722417225172261722717228172291723017231172321723317234172351723617237172381723917240172411724217243172441724517246172471724817249172501725117252172531725417255172561725717258172591726017261172621726317264172651726617267172681726917270172711727217273172741727517276172771727817279172801728117282172831728417285172861728717288172891729017291172921729317294172951729617297172981729917300173011730217303173041730517306173071730817309173101731117312173131731417315173161731717318173191732017321173221732317324173251732617327173281732917330173311733217333173341733517336173371733817339173401734117342173431734417345173461734717348173491735017351173521735317354173551735617357173581735917360173611736217363173641736517366173671736817369173701737117372173731737417375173761737717378173791738017381173821738317384173851738617387173881738917390173911739217393173941739517396173971739817399174001740117402174031740417405174061740717408174091741017411174121741317414174151741617417174181741917420174211742217423174241742517426174271742817429174301743117432174331743417435174361743717438174391744017441174421744317444174451744617447174481744917450174511745217453174541745517456174571745817459174601746117462174631746417465174661746717468174691747017471174721747317474174751747617477174781747917480174811748217483174841748517486174871748817489174901749117492174931749417495174961749717498174991750017501175021750317504175051750617507175081750917510175111751217513175141751517516175171751817519175201752117522175231752417525175261752717528175291753017531175321753317534175351753617537175381753917540175411754217543175441754517546175471754817549175501755117552175531755417555175561755717558175591756017561175621756317564175651756617567175681756917570175711757217573175741757517576175771757817579175801758117582175831758417585175861758717588175891759017591175921759317594175951759617597175981759917600176011760217603176041760517606176071760817609176101761117612176131761417615176161761717618176191762017621176221762317624176251762617627176281762917630176311763217633176341763517636176371763817639176401764117642176431764417645176461764717648176491765017651176521765317654176551765617657176581765917660176611766217663176641766517666176671766817669176701767117672176731767417675176761767717678176791768017681176821768317684176851768617687176881768917690176911769217693176941769517696176971769817699177001770117702177031770417705177061770717708177091771017711177121771317714177151771617717177181771917720177211772217723177241772517726177271772817729177301773117732177331773417735177361773717738177391774017741177421774317744177451774617747177481774917750177511775217753177541775517756177571775817759177601776117762177631776417765177661776717768177691777017771177721777317774177751777617777177781777917780177811778217783177841778517786177871778817789177901779117792177931779417795177961779717798177991780017801178021780317804178051780617807178081780917810178111781217813178141781517816178171781817819178201782117822178231782417825178261782717828178291783017831178321783317834178351783617837178381783917840178411784217843178441784517846178471784817849178501785117852178531785417855178561785717858178591786017861178621786317864178651786617867178681786917870178711787217873178741787517876178771787817879178801788117882178831788417885178861788717888178891789017891178921789317894178951789617897178981789917900179011790217903179041790517906179071790817909179101791117912179131791417915179161791717918179191792017921179221792317924179251792617927179281792917930179311793217933179341793517936179371793817939179401794117942179431794417945179461794717948179491795017951179521795317954179551795617957179581795917960179611796217963179641796517966179671796817969179701797117972179731797417975179761797717978179791798017981179821798317984179851798617987179881798917990179911799217993179941799517996179971799817999180001800118002180031800418005180061800718008180091801018011180121801318014180151801618017180181801918020180211802218023180241802518026180271802818029180301803118032180331803418035180361803718038180391804018041180421804318044180451804618047180481804918050180511805218053180541805518056180571805818059180601806118062180631806418065180661806718068180691807018071180721807318074180751807618077180781807918080180811808218083180841808518086180871808818089180901809118092180931809418095180961809718098180991810018101181021810318104181051810618107181081810918110181111811218113181141811518116181171811818119181201812118122181231812418125181261812718128181291813018131181321813318134181351813618137181381813918140181411814218143181441814518146181471814818149181501815118152181531815418155181561815718158181591816018161181621816318164181651816618167181681816918170181711817218173181741817518176181771817818179181801818118182181831818418185181861818718188181891819018191181921819318194181951819618197181981819918200182011820218203182041820518206182071820818209182101821118212182131821418215182161821718218182191822018221182221822318224182251822618227182281822918230182311823218233182341823518236182371823818239182401824118242182431824418245182461824718248182491825018251182521825318254182551825618257182581825918260182611826218263182641826518266182671826818269182701827118272182731827418275182761827718278182791828018281182821828318284182851828618287182881828918290182911829218293182941829518296182971829818299183001830118302183031830418305183061830718308183091831018311183121831318314183151831618317183181831918320183211832218323183241832518326183271832818329183301833118332183331833418335183361833718338183391834018341183421834318344183451834618347183481834918350183511835218353183541835518356183571835818359183601836118362183631836418365183661836718368183691837018371183721837318374183751837618377183781837918380183811838218383183841838518386183871838818389183901839118392183931839418395183961839718398183991840018401184021840318404184051840618407184081840918410184111841218413184141841518416184171841818419184201842118422184231842418425184261842718428184291843018431184321843318434184351843618437184381843918440184411844218443184441844518446184471844818449184501845118452184531845418455184561845718458184591846018461184621846318464184651846618467184681846918470184711847218473184741847518476184771847818479184801848118482184831848418485184861848718488184891849018491184921849318494184951849618497184981849918500185011850218503185041850518506185071850818509185101851118512185131851418515185161851718518185191852018521185221852318524185251852618527185281852918530185311853218533185341853518536185371853818539185401854118542185431854418545185461854718548185491855018551185521855318554185551855618557185581855918560185611856218563185641856518566185671856818569185701857118572185731857418575185761857718578185791858018581185821858318584185851858618587185881858918590185911859218593185941859518596185971859818599186001860118602186031860418605186061860718608186091861018611186121861318614186151861618617186181861918620186211862218623186241862518626186271862818629186301863118632186331863418635186361863718638186391864018641186421864318644186451864618647186481864918650186511865218653186541865518656186571865818659186601866118662186631866418665186661866718668186691867018671186721867318674186751867618677186781867918680186811868218683186841868518686186871868818689186901869118692186931869418695186961869718698186991870018701187021870318704187051870618707187081870918710187111871218713187141871518716187171871818719187201872118722187231872418725187261872718728187291873018731187321873318734187351873618737187381873918740187411874218743187441874518746187471874818749187501875118752187531875418755187561875718758187591876018761187621876318764187651876618767187681876918770187711877218773187741877518776187771877818779187801878118782187831878418785187861878718788187891879018791187921879318794187951879618797187981879918800188011880218803188041880518806188071880818809188101881118812188131881418815188161881718818188191882018821188221882318824188251882618827188281882918830188311883218833188341883518836188371883818839188401884118842188431884418845188461884718848188491885018851188521885318854188551885618857188581885918860188611886218863188641886518866188671886818869188701887118872188731887418875188761887718878188791888018881188821888318884188851888618887188881888918890188911889218893188941889518896188971889818899189001890118902189031890418905189061890718908189091891018911189121891318914189151891618917189181891918920189211892218923189241892518926189271892818929189301893118932189331893418935189361893718938189391894018941189421894318944189451894618947189481894918950189511895218953189541895518956189571895818959189601896118962189631896418965189661896718968189691897018971189721897318974189751897618977189781897918980189811898218983189841898518986189871898818989189901899118992189931899418995189961899718998189991900019001190021900319004190051900619007190081900919010190111901219013190141901519016190171901819019190201902119022190231902419025190261902719028190291903019031190321903319034190351903619037190381903919040190411904219043190441904519046190471904819049190501905119052190531905419055190561905719058190591906019061190621906319064190651906619067190681906919070190711907219073190741907519076190771907819079190801908119082190831908419085190861908719088190891909019091190921909319094190951909619097190981909919100191011910219103191041910519106191071910819109191101911119112191131911419115191161911719118191191912019121191221912319124191251912619127191281912919130191311913219133191341913519136191371913819139191401914119142191431914419145191461914719148191491915019151191521915319154191551915619157191581915919160191611916219163191641916519166191671916819169191701917119172191731917419175191761917719178191791918019181191821918319184191851918619187191881918919190191911919219193191941919519196191971919819199192001920119202192031920419205192061920719208192091921019211192121921319214192151921619217192181921919220192211922219223192241922519226192271922819229192301923119232192331923419235192361923719238192391924019241192421924319244192451924619247192481924919250192511925219253192541925519256192571925819259192601926119262192631926419265192661926719268192691927019271192721927319274192751927619277192781927919280192811928219283192841928519286192871928819289192901929119292192931929419295192961929719298192991930019301193021930319304193051930619307193081930919310193111931219313193141931519316193171931819319193201932119322193231932419325193261932719328193291933019331193321933319334193351933619337193381933919340193411934219343193441934519346193471934819349193501935119352193531935419355193561935719358193591936019361193621936319364193651936619367193681936919370193711937219373193741937519376193771937819379193801938119382193831938419385193861938719388193891939019391193921939319394193951939619397193981939919400194011940219403194041940519406194071940819409194101941119412194131941419415194161941719418194191942019421194221942319424194251942619427194281942919430194311943219433194341943519436194371943819439194401944119442194431944419445194461944719448194491945019451194521945319454194551945619457194581945919460194611946219463194641946519466194671946819469194701947119472194731947419475194761947719478194791948019481194821948319484194851948619487194881948919490194911949219493194941949519496194971949819499195001950119502195031950419505195061950719508195091951019511195121951319514195151951619517195181951919520195211952219523195241952519526195271952819529195301953119532195331953419535195361953719538195391954019541195421954319544195451954619547195481954919550195511955219553195541955519556195571955819559195601956119562195631956419565195661956719568195691957019571195721957319574195751957619577195781957919580195811958219583195841958519586195871958819589195901959119592195931959419595195961959719598195991960019601196021960319604196051960619607196081960919610196111961219613196141961519616196171961819619196201962119622196231962419625196261962719628196291963019631196321963319634196351963619637196381963919640196411964219643196441964519646196471964819649196501965119652196531965419655196561965719658196591966019661196621966319664196651966619667196681966919670196711967219673196741967519676196771967819679196801968119682196831968419685196861968719688196891969019691196921969319694196951969619697196981969919700197011970219703197041970519706197071970819709197101971119712197131971419715197161971719718197191972019721197221972319724197251972619727197281972919730197311973219733197341973519736197371973819739197401974119742197431974419745197461974719748197491975019751197521975319754197551975619757197581975919760197611976219763197641976519766197671976819769197701977119772197731977419775197761977719778197791978019781197821978319784197851978619787197881978919790197911979219793197941979519796197971979819799198001980119802198031980419805198061980719808198091981019811198121981319814198151981619817198181981919820198211982219823198241982519826198271982819829198301983119832198331983419835198361983719838198391984019841198421984319844198451984619847198481984919850198511985219853198541985519856198571985819859198601986119862198631986419865198661986719868198691987019871198721987319874198751987619877198781987919880198811988219883198841988519886198871988819889198901989119892198931989419895198961989719898198991990019901199021990319904199051990619907199081990919910199111991219913199141991519916199171991819919199201992119922199231992419925199261992719928199291993019931199321993319934199351993619937199381993919940199411994219943199441994519946199471994819949199501995119952199531995419955199561995719958199591996019961199621996319964199651996619967199681996919970199711997219973199741997519976199771997819979199801998119982199831998419985199861998719988199891999019991199921999319994199951999619997199981999920000200012000220003200042000520006200072000820009200102001120012200132001420015200162001720018200192002020021200222002320024200252002620027200282002920030200312003220033200342003520036200372003820039200402004120042200432004420045200462004720048200492005020051200522005320054200552005620057200582005920060200612006220063200642006520066200672006820069200702007120072200732007420075200762007720078200792008020081200822008320084200852008620087200882008920090200912009220093200942009520096200972009820099201002010120102201032010420105201062010720108201092011020111201122011320114201152011620117201182011920120201212012220123201242012520126201272012820129201302013120132201332013420135201362013720138201392014020141201422014320144201452014620147201482014920150201512015220153201542015520156201572015820159201602016120162201632016420165201662016720168201692017020171201722017320174201752017620177201782017920180201812018220183201842018520186201872018820189201902019120192201932019420195201962019720198201992020020201202022020320204202052020620207202082020920210202112021220213202142021520216202172021820219202202022120222202232022420225202262022720228202292023020231202322023320234202352023620237202382023920240202412024220243202442024520246202472024820249202502025120252202532025420255202562025720258202592026020261202622026320264202652026620267202682026920270202712027220273202742027520276202772027820279202802028120282202832028420285202862028720288202892029020291202922029320294202952029620297202982029920300203012030220303203042030520306203072030820309203102031120312203132031420315203162031720318203192032020321203222032320324203252032620327203282032920330203312033220333203342033520336203372033820339203402034120342203432034420345203462034720348203492035020351203522035320354203552035620357203582035920360203612036220363203642036520366203672036820369203702037120372203732037420375203762037720378203792038020381203822038320384203852038620387203882038920390203912039220393203942039520396203972039820399204002040120402204032040420405204062040720408204092041020411204122041320414204152041620417204182041920420204212042220423204242042520426204272042820429204302043120432204332043420435204362043720438204392044020441204422044320444204452044620447204482044920450204512045220453204542045520456204572045820459204602046120462204632046420465204662046720468204692047020471204722047320474204752047620477204782047920480204812048220483204842048520486204872048820489204902049120492204932049420495204962049720498204992050020501205022050320504205052050620507205082050920510205112051220513205142051520516205172051820519205202052120522205232052420525205262052720528205292053020531205322053320534205352053620537205382053920540205412054220543205442054520546205472054820549205502055120552205532055420555205562055720558205592056020561205622056320564205652056620567205682056920570205712057220573205742057520576205772057820579205802058120582205832058420585205862058720588205892059020591205922059320594205952059620597205982059920600206012060220603206042060520606206072060820609206102061120612206132061420615206162061720618206192062020621206222062320624206252062620627206282062920630206312063220633206342063520636206372063820639206402064120642206432064420645206462064720648206492065020651206522065320654206552065620657206582065920660206612066220663206642066520666206672066820669206702067120672206732067420675206762067720678206792068020681206822068320684206852068620687206882068920690206912069220693206942069520696206972069820699207002070120702207032070420705207062070720708207092071020711207122071320714207152071620717207182071920720207212072220723207242072520726207272072820729207302073120732207332073420735207362073720738207392074020741207422074320744207452074620747207482074920750207512075220753207542075520756207572075820759207602076120762207632076420765207662076720768207692077020771207722077320774207752077620777207782077920780207812078220783207842078520786207872078820789207902079120792207932079420795207962079720798207992080020801208022080320804208052080620807208082080920810208112081220813208142081520816208172081820819208202082120822208232082420825208262082720828208292083020831208322083320834208352083620837208382083920840208412084220843208442084520846208472084820849208502085120852208532085420855208562085720858208592086020861208622086320864208652086620867208682086920870208712087220873208742087520876208772087820879208802088120882208832088420885208862088720888208892089020891208922089320894208952089620897208982089920900209012090220903209042090520906209072090820909209102091120912209132091420915209162091720918209192092020921209222092320924209252092620927209282092920930209312093220933209342093520936209372093820939209402094120942209432094420945209462094720948209492095020951209522095320954209552095620957209582095920960209612096220963209642096520966209672096820969209702097120972209732097420975209762097720978209792098020981209822098320984209852098620987209882098920990209912099220993209942099520996209972099820999210002100121002210032100421005210062100721008210092101021011210122101321014210152101621017210182101921020210212102221023210242102521026210272102821029210302103121032210332103421035210362103721038210392104021041210422104321044210452104621047210482104921050210512105221053210542105521056210572105821059210602106121062210632106421065210662106721068210692107021071210722107321074210752107621077210782107921080210812108221083210842108521086210872108821089210902109121092210932109421095210962109721098210992110021101211022110321104211052110621107211082110921110211112111221113211142111521116211172111821119211202112121122211232112421125211262112721128211292113021131211322113321134211352113621137211382113921140211412114221143211442114521146211472114821149211502115121152211532115421155211562115721158211592116021161211622116321164211652116621167211682116921170211712117221173211742117521176211772117821179211802118121182211832118421185211862118721188211892119021191211922119321194211952119621197211982119921200212012120221203212042120521206212072120821209212102121121212212132121421215212162121721218212192122021221212222122321224212252122621227212282122921230212312123221233212342123521236212372123821239212402124121242212432124421245212462124721248212492125021251212522125321254212552125621257212582125921260212612126221263212642126521266212672126821269212702127121272212732127421275212762127721278212792128021281212822128321284212852128621287212882128921290212912129221293212942129521296212972129821299213002130121302213032130421305213062130721308213092131021311213122131321314213152131621317213182131921320213212132221323213242132521326213272132821329213302133121332213332133421335213362133721338213392134021341213422134321344213452134621347213482134921350213512135221353213542135521356213572135821359213602136121362213632136421365213662136721368213692137021371213722137321374213752137621377213782137921380213812138221383213842138521386213872138821389213902139121392213932139421395213962139721398213992140021401214022140321404214052140621407214082140921410214112141221413214142141521416214172141821419214202142121422214232142421425214262142721428214292143021431214322143321434214352143621437214382143921440214412144221443214442144521446214472144821449214502145121452214532145421455214562145721458214592146021461214622146321464214652146621467214682146921470214712147221473214742147521476214772147821479214802148121482214832148421485214862148721488214892149021491214922149321494214952149621497214982149921500215012150221503215042150521506215072150821509215102151121512215132151421515215162151721518215192152021521215222152321524215252152621527215282152921530215312153221533215342153521536215372153821539215402154121542215432154421545215462154721548215492155021551215522155321554215552155621557215582155921560215612156221563215642156521566215672156821569215702157121572215732157421575215762157721578215792158021581215822158321584215852158621587215882158921590215912159221593215942159521596215972159821599216002160121602216032160421605216062160721608216092161021611216122161321614216152161621617216182161921620216212162221623216242162521626216272162821629216302163121632216332163421635216362163721638216392164021641216422164321644216452164621647216482164921650216512165221653216542165521656216572165821659216602166121662216632166421665216662166721668216692167021671216722167321674216752167621677216782167921680216812168221683216842168521686216872168821689216902169121692216932169421695216962169721698216992170021701217022170321704217052170621707217082170921710217112171221713217142171521716217172171821719217202172121722217232172421725217262172721728217292173021731217322173321734217352173621737217382173921740217412174221743217442174521746217472174821749217502175121752217532175421755217562175721758217592176021761217622176321764217652176621767217682176921770217712177221773217742177521776217772177821779217802178121782217832178421785217862178721788217892179021791217922179321794217952179621797217982179921800218012180221803218042180521806218072180821809218102181121812218132181421815218162181721818218192182021821218222182321824218252182621827218282182921830218312183221833218342183521836218372183821839218402184121842218432184421845218462184721848218492185021851218522185321854218552185621857218582185921860218612186221863218642186521866218672186821869218702187121872218732187421875218762187721878218792188021881218822188321884218852188621887218882188921890218912189221893218942189521896218972189821899219002190121902219032190421905219062190721908219092191021911219122191321914219152191621917219182191921920219212192221923219242192521926219272192821929219302193121932219332193421935219362193721938219392194021941219422194321944219452194621947219482194921950219512195221953219542195521956219572195821959219602196121962219632196421965219662196721968219692197021971219722197321974219752197621977219782197921980219812198221983219842198521986219872198821989219902199121992219932199421995219962199721998219992200022001220022200322004220052200622007220082200922010220112201222013220142201522016220172201822019220202202122022220232202422025220262202722028220292203022031220322203322034220352203622037220382203922040220412204222043220442204522046220472204822049220502205122052220532205422055220562205722058220592206022061220622206322064220652206622067220682206922070220712207222073220742207522076220772207822079220802208122082220832208422085220862208722088220892209022091220922209322094220952209622097220982209922100221012210222103221042210522106221072210822109221102211122112221132211422115221162211722118221192212022121221222212322124221252212622127221282212922130221312213222133221342213522136221372213822139221402214122142221432214422145221462214722148221492215022151221522215322154221552215622157221582215922160221612216222163221642216522166221672216822169221702217122172221732217422175221762217722178221792218022181221822218322184221852218622187221882218922190221912219222193221942219522196221972219822199222002220122202222032220422205222062220722208222092221022211222122221322214222152221622217222182221922220222212222222223222242222522226222272222822229222302223122232222332223422235222362223722238222392224022241222422224322244222452224622247222482224922250222512225222253222542225522256222572225822259222602226122262222632226422265222662226722268222692227022271222722227322274222752227622277222782227922280222812228222283222842228522286222872228822289222902229122292222932229422295222962229722298222992230022301223022230322304223052230622307223082230922310223112231222313223142231522316223172231822319223202232122322223232232422325223262232722328223292233022331223322233322334223352233622337223382233922340223412234222343223442234522346223472234822349223502235122352223532235422355223562235722358223592236022361223622236322364223652236622367223682236922370223712237222373223742237522376223772237822379223802238122382223832238422385223862238722388223892239022391223922239322394223952239622397223982239922400224012240222403224042240522406224072240822409224102241122412224132241422415224162241722418224192242022421224222242322424224252242622427224282242922430224312243222433224342243522436224372243822439224402244122442224432244422445224462244722448224492245022451224522245322454224552245622457224582245922460224612246222463224642246522466224672246822469224702247122472224732247422475224762247722478224792248022481224822248322484224852248622487224882248922490224912249222493224942249522496224972249822499225002250122502225032250422505225062250722508225092251022511225122251322514225152251622517225182251922520225212252222523225242252522526225272252822529225302253122532225332253422535225362253722538225392254022541225422254322544225452254622547225482254922550225512255222553225542255522556225572255822559225602256122562225632256422565225662256722568225692257022571225722257322574225752257622577225782257922580225812258222583225842258522586225872258822589225902259122592225932259422595225962259722598225992260022601226022260322604226052260622607226082260922610226112261222613226142261522616226172261822619226202262122622226232262422625226262262722628226292263022631226322263322634226352263622637226382263922640226412264222643226442264522646226472264822649226502265122652226532265422655226562265722658226592266022661226622266322664226652266622667226682266922670226712267222673226742267522676226772267822679226802268122682226832268422685226862268722688226892269022691226922269322694226952269622697226982269922700227012270222703227042270522706227072270822709227102271122712227132271422715227162271722718227192272022721227222272322724227252272622727227282272922730227312273222733227342273522736227372273822739227402274122742227432274422745227462274722748227492275022751227522275322754227552275622757227582275922760227612276222763227642276522766227672276822769227702277122772227732277422775227762277722778227792278022781227822278322784227852278622787227882278922790227912279222793227942279522796227972279822799228002280122802228032280422805228062280722808228092281022811228122281322814228152281622817228182281922820228212282222823228242282522826228272282822829228302283122832228332283422835228362283722838228392284022841228422284322844228452284622847228482284922850228512285222853228542285522856228572285822859228602286122862228632286422865228662286722868228692287022871228722287322874228752287622877228782287922880228812288222883228842288522886228872288822889228902289122892228932289422895228962289722898228992290022901229022290322904229052290622907229082290922910229112291222913229142291522916229172291822919229202292122922229232292422925229262292722928229292293022931229322293322934229352293622937229382293922940229412294222943229442294522946229472294822949229502295122952229532295422955229562295722958229592296022961229622296322964229652296622967229682296922970229712297222973229742297522976229772297822979229802298122982229832298422985229862298722988229892299022991229922299322994229952299622997229982299923000230012300223003230042300523006230072300823009230102301123012230132301423015230162301723018230192302023021230222302323024230252302623027230282302923030230312303223033230342303523036230372303823039230402304123042230432304423045230462304723048230492305023051230522305323054230552305623057230582305923060230612306223063230642306523066230672306823069230702307123072230732307423075230762307723078230792308023081230822308323084230852308623087230882308923090230912309223093230942309523096230972309823099231002310123102231032310423105231062310723108231092311023111231122311323114231152311623117231182311923120231212312223123231242312523126231272312823129231302313123132231332313423135231362313723138231392314023141231422314323144231452314623147231482314923150231512315223153231542315523156231572315823159231602316123162231632316423165231662316723168231692317023171231722317323174231752317623177231782317923180231812318223183231842318523186231872318823189231902319123192231932319423195231962319723198231992320023201232022320323204232052320623207232082320923210232112321223213232142321523216232172321823219232202322123222232232322423225232262322723228232292323023231232322323323234232352323623237232382323923240232412324223243232442324523246232472324823249232502325123252232532325423255232562325723258232592326023261232622326323264232652326623267232682326923270232712327223273232742327523276232772327823279232802328123282232832328423285232862328723288232892329023291232922329323294232952329623297232982329923300233012330223303233042330523306233072330823309233102331123312233132331423315233162331723318233192332023321233222332323324233252332623327233282332923330233312333223333233342333523336233372333823339233402334123342233432334423345233462334723348233492335023351233522335323354233552335623357233582335923360233612336223363233642336523366233672336823369233702337123372233732337423375233762337723378233792338023381233822338323384233852338623387233882338923390233912339223393233942339523396233972339823399234002340123402234032340423405234062340723408234092341023411234122341323414234152341623417234182341923420234212342223423234242342523426234272342823429234302343123432234332343423435234362343723438234392344023441234422344323444234452344623447234482344923450234512345223453234542345523456234572345823459234602346123462234632346423465234662346723468234692347023471234722347323474234752347623477234782347923480234812348223483234842348523486234872348823489234902349123492234932349423495234962349723498234992350023501235022350323504235052350623507235082350923510235112351223513235142351523516235172351823519235202352123522235232352423525235262352723528235292353023531235322353323534235352353623537235382353923540235412354223543235442354523546235472354823549235502355123552235532355423555235562355723558235592356023561235622356323564235652356623567235682356923570235712357223573235742357523576235772357823579235802358123582235832358423585235862358723588235892359023591235922359323594235952359623597235982359923600236012360223603236042360523606236072360823609236102361123612236132361423615236162361723618236192362023621236222362323624236252362623627236282362923630236312363223633236342363523636236372363823639236402364123642236432364423645236462364723648236492365023651236522365323654236552365623657236582365923660236612366223663236642366523666236672366823669236702367123672236732367423675236762367723678236792368023681236822368323684236852368623687236882368923690236912369223693236942369523696236972369823699237002370123702237032370423705237062370723708237092371023711237122371323714237152371623717237182371923720237212372223723237242372523726237272372823729237302373123732237332373423735237362373723738237392374023741237422374323744237452374623747237482374923750237512375223753237542375523756237572375823759237602376123762237632376423765237662376723768237692377023771237722377323774237752377623777237782377923780237812378223783237842378523786237872378823789237902379123792237932379423795237962379723798237992380023801238022380323804238052380623807238082380923810238112381223813238142381523816238172381823819238202382123822238232382423825238262382723828238292383023831238322383323834238352383623837238382383923840238412384223843238442384523846238472384823849238502385123852238532385423855238562385723858238592386023861238622386323864238652386623867238682386923870238712387223873238742387523876238772387823879238802388123882238832388423885238862388723888238892389023891238922389323894238952389623897238982389923900239012390223903239042390523906239072390823909239102391123912239132391423915239162391723918239192392023921239222392323924239252392623927239282392923930239312393223933239342393523936239372393823939239402394123942239432394423945239462394723948239492395023951239522395323954239552395623957239582395923960239612396223963239642396523966239672396823969239702397123972239732397423975239762397723978239792398023981239822398323984239852398623987239882398923990239912399223993239942399523996239972399823999240002400124002240032400424005240062400724008240092401024011240122401324014240152401624017240182401924020240212402224023240242402524026240272402824029240302403124032240332403424035240362403724038240392404024041240422404324044240452404624047240482404924050240512405224053240542405524056240572405824059240602406124062240632406424065240662406724068240692407024071240722407324074240752407624077240782407924080240812408224083240842408524086240872408824089240902409124092240932409424095240962409724098240992410024101241022410324104241052410624107241082410924110241112411224113241142411524116241172411824119241202412124122241232412424125241262412724128241292413024131241322413324134241352413624137241382413924140241412414224143241442414524146241472414824149241502415124152241532415424155241562415724158241592416024161241622416324164241652416624167241682416924170241712417224173241742417524176241772417824179241802418124182241832418424185241862418724188241892419024191241922419324194241952419624197241982419924200242012420224203242042420524206242072420824209242102421124212242132421424215242162421724218242192422024221242222422324224242252422624227242282422924230242312423224233242342423524236242372423824239242402424124242242432424424245242462424724248242492425024251242522425324254242552425624257242582425924260242612426224263242642426524266242672426824269242702427124272242732427424275242762427724278242792428024281242822428324284242852428624287242882428924290242912429224293242942429524296242972429824299243002430124302243032430424305243062430724308243092431024311243122431324314243152431624317243182431924320243212432224323243242432524326243272432824329243302433124332243332433424335243362433724338243392434024341243422434324344243452434624347243482434924350243512435224353243542435524356243572435824359243602436124362243632436424365243662436724368243692437024371243722437324374243752437624377243782437924380243812438224383243842438524386243872438824389243902439124392243932439424395243962439724398243992440024401244022440324404244052440624407244082440924410244112441224413244142441524416244172441824419244202442124422244232442424425244262442724428244292443024431244322443324434244352443624437244382443924440244412444224443244442444524446244472444824449244502445124452244532445424455244562445724458244592446024461244622446324464244652446624467244682446924470244712447224473244742447524476244772447824479244802448124482244832448424485244862448724488244892449024491244922449324494244952449624497244982449924500245012450224503245042450524506245072450824509245102451124512245132451424515245162451724518245192452024521245222452324524245252452624527245282452924530245312453224533245342453524536245372453824539245402454124542245432454424545245462454724548245492455024551245522455324554245552455624557245582455924560245612456224563245642456524566245672456824569245702457124572245732457424575245762457724578245792458024581245822458324584245852458624587245882458924590245912459224593245942459524596245972459824599246002460124602246032460424605246062460724608246092461024611246122461324614246152461624617246182461924620246212462224623246242462524626246272462824629246302463124632246332463424635246362463724638246392464024641246422464324644246452464624647246482464924650246512465224653246542465524656246572465824659246602466124662246632466424665246662466724668246692467024671246722467324674246752467624677246782467924680246812468224683246842468524686246872468824689246902469124692246932469424695246962469724698246992470024701247022470324704247052470624707247082470924710247112471224713247142471524716247172471824719247202472124722247232472424725247262472724728247292473024731247322473324734247352473624737247382473924740247412474224743247442474524746247472474824749247502475124752247532475424755247562475724758247592476024761247622476324764247652476624767247682476924770247712477224773247742477524776247772477824779247802478124782247832478424785247862478724788247892479024791247922479324794247952479624797247982479924800248012480224803248042480524806248072480824809248102481124812248132481424815248162481724818248192482024821248222482324824248252482624827248282482924830248312483224833248342483524836248372483824839248402484124842248432484424845248462484724848248492485024851248522485324854248552485624857248582485924860248612486224863248642486524866248672486824869248702487124872248732487424875248762487724878248792488024881248822488324884248852488624887248882488924890248912489224893248942489524896248972489824899249002490124902249032490424905249062490724908249092491024911249122491324914249152491624917249182491924920249212492224923249242492524926249272492824929249302493124932249332493424935249362493724938249392494024941249422494324944249452494624947249482494924950249512495224953249542495524956249572495824959249602496124962249632496424965249662496724968249692497024971249722497324974249752497624977249782497924980249812498224983249842498524986249872498824989249902499124992249932499424995249962499724998249992500025001250022500325004250052500625007250082500925010250112501225013250142501525016250172501825019250202502125022250232502425025250262502725028250292503025031250322503325034250352503625037250382503925040250412504225043250442504525046250472504825049250502505125052250532505425055250562505725058250592506025061250622506325064250652506625067250682506925070250712507225073250742507525076250772507825079250802508125082250832508425085250862508725088250892509025091250922509325094250952509625097250982509925100251012510225103251042510525106251072510825109251102511125112251132511425115251162511725118251192512025121251222512325124251252512625127251282512925130251312513225133251342513525136251372513825139251402514125142251432514425145251462514725148251492515025151251522515325154251552515625157251582515925160251612516225163251642516525166251672516825169251702517125172251732517425175251762517725178251792518025181251822518325184251852518625187251882518925190251912519225193251942519525196251972519825199252002520125202252032520425205252062520725208252092521025211252122521325214252152521625217252182521925220252212522225223252242522525226252272522825229252302523125232252332523425235252362523725238252392524025241252422524325244252452524625247252482524925250252512525225253252542525525256252572525825259252602526125262252632526425265252662526725268252692527025271252722527325274252752527625277252782527925280252812528225283252842528525286252872528825289252902529125292252932529425295252962529725298252992530025301253022530325304253052530625307253082530925310253112531225313253142531525316253172531825319253202532125322253232532425325253262532725328253292533025331253322533325334253352533625337253382533925340253412534225343253442534525346253472534825349253502535125352253532535425355253562535725358253592536025361253622536325364253652536625367253682536925370253712537225373253742537525376253772537825379253802538125382253832538425385253862538725388253892539025391253922539325394253952539625397253982539925400254012540225403254042540525406254072540825409254102541125412254132541425415254162541725418254192542025421254222542325424254252542625427254282542925430254312543225433254342543525436254372543825439254402544125442254432544425445254462544725448254492545025451254522545325454254552545625457254582545925460254612546225463254642546525466254672546825469254702547125472254732547425475254762547725478254792548025481254822548325484254852548625487254882548925490254912549225493254942549525496254972549825499255002550125502255032550425505255062550725508255092551025511255122551325514255152551625517255182551925520255212552225523255242552525526255272552825529255302553125532255332553425535255362553725538255392554025541255422554325544255452554625547255482554925550255512555225553255542555525556255572555825559255602556125562255632556425565255662556725568255692557025571255722557325574255752557625577255782557925580255812558225583255842558525586255872558825589255902559125592255932559425595255962559725598255992560025601256022560325604256052560625607256082560925610256112561225613256142561525616256172561825619256202562125622256232562425625256262562725628256292563025631256322563325634256352563625637256382563925640256412564225643256442564525646256472564825649256502565125652256532565425655256562565725658256592566025661256622566325664256652566625667256682566925670256712567225673256742567525676256772567825679256802568125682256832568425685256862568725688256892569025691256922569325694256952569625697256982569925700257012570225703257042570525706257072570825709257102571125712257132571425715257162571725718257192572025721257222572325724257252572625727257282572925730257312573225733257342573525736257372573825739257402574125742257432574425745257462574725748257492575025751257522575325754257552575625757257582575925760257612576225763257642576525766257672576825769257702577125772257732577425775257762577725778257792578025781257822578325784257852578625787257882578925790257912579225793257942579525796257972579825799258002580125802258032580425805258062580725808258092581025811258122581325814258152581625817258182581925820258212582225823258242582525826258272582825829258302583125832258332583425835258362583725838258392584025841258422584325844258452584625847258482584925850258512585225853258542585525856258572585825859258602586125862258632586425865258662586725868258692587025871258722587325874258752587625877258782587925880258812588225883258842588525886258872588825889258902589125892258932589425895258962589725898258992590025901259022590325904259052590625907259082590925910259112591225913259142591525916259172591825919259202592125922259232592425925259262592725928259292593025931259322593325934259352593625937259382593925940259412594225943259442594525946259472594825949259502595125952259532595425955259562595725958259592596025961259622596325964259652596625967259682596925970259712597225973259742597525976259772597825979259802598125982259832598425985259862598725988259892599025991259922599325994259952599625997259982599926000260012600226003260042600526006260072600826009260102601126012260132601426015260162601726018260192602026021260222602326024260252602626027260282602926030260312603226033260342603526036260372603826039260402604126042260432604426045260462604726048260492605026051260522605326054260552605626057260582605926060260612606226063260642606526066260672606826069260702607126072260732607426075260762607726078260792608026081260822608326084260852608626087260882608926090260912609226093260942609526096260972609826099261002610126102261032610426105261062610726108261092611026111261122611326114261152611626117261182611926120261212612226123261242612526126261272612826129261302613126132261332613426135261362613726138261392614026141261422614326144261452614626147261482614926150261512615226153261542615526156261572615826159261602616126162261632616426165261662616726168261692617026171261722617326174261752617626177261782617926180261812618226183261842618526186261872618826189261902619126192261932619426195261962619726198261992620026201262022620326204262052620626207262082620926210262112621226213262142621526216262172621826219262202622126222262232622426225262262622726228262292623026231262322623326234262352623626237262382623926240262412624226243262442624526246262472624826249262502625126252262532625426255262562625726258262592626026261262622626326264262652626626267262682626926270262712627226273262742627526276262772627826279262802628126282262832628426285262862628726288262892629026291262922629326294262952629626297262982629926300263012630226303263042630526306263072630826309263102631126312263132631426315263162631726318263192632026321263222632326324263252632626327263282632926330263312633226333263342633526336263372633826339263402634126342263432634426345263462634726348263492635026351263522635326354263552635626357263582635926360263612636226363263642636526366263672636826369263702637126372263732637426375263762637726378263792638026381263822638326384263852638626387263882638926390263912639226393263942639526396263972639826399264002640126402264032640426405264062640726408264092641026411264122641326414264152641626417264182641926420264212642226423264242642526426264272642826429264302643126432264332643426435264362643726438264392644026441264422644326444264452644626447264482644926450264512645226453264542645526456264572645826459264602646126462264632646426465264662646726468264692647026471264722647326474264752647626477264782647926480264812648226483264842648526486264872648826489264902649126492264932649426495264962649726498264992650026501265022650326504265052650626507265082650926510265112651226513265142651526516265172651826519265202652126522265232652426525265262652726528265292653026531265322653326534265352653626537265382653926540265412654226543265442654526546265472654826549265502655126552265532655426555265562655726558265592656026561265622656326564265652656626567265682656926570265712657226573265742657526576265772657826579265802658126582265832658426585265862658726588265892659026591265922659326594265952659626597265982659926600266012660226603266042660526606266072660826609266102661126612266132661426615266162661726618266192662026621266222662326624266252662626627266282662926630266312663226633266342663526636266372663826639266402664126642266432664426645266462664726648266492665026651266522665326654266552665626657266582665926660266612666226663266642666526666266672666826669266702667126672266732667426675266762667726678266792668026681266822668326684266852668626687266882668926690266912669226693266942669526696266972669826699267002670126702267032670426705267062670726708267092671026711267122671326714267152671626717267182671926720267212672226723267242672526726267272672826729267302673126732267332673426735267362673726738267392674026741267422674326744267452674626747267482674926750267512675226753267542675526756267572675826759267602676126762267632676426765267662676726768267692677026771267722677326774267752677626777267782677926780267812678226783267842678526786267872678826789267902679126792267932679426795267962679726798267992680026801268022680326804268052680626807268082680926810268112681226813268142681526816268172681826819268202682126822268232682426825268262682726828268292683026831268322683326834268352683626837268382683926840268412684226843268442684526846268472684826849268502685126852268532685426855268562685726858268592686026861268622686326864268652686626867268682686926870268712687226873268742687526876268772687826879268802688126882268832688426885268862688726888268892689026891268922689326894268952689626897268982689926900269012690226903269042690526906269072690826909269102691126912269132691426915269162691726918269192692026921269222692326924269252692626927269282692926930269312693226933269342693526936269372693826939269402694126942269432694426945269462694726948269492695026951269522695326954269552695626957269582695926960269612696226963269642696526966269672696826969269702697126972269732697426975269762697726978269792698026981269822698326984269852698626987269882698926990269912699226993269942699526996269972699826999270002700127002270032700427005270062700727008270092701027011270122701327014270152701627017270182701927020270212702227023270242702527026270272702827029270302703127032270332703427035270362703727038270392704027041270422704327044270452704627047270482704927050270512705227053270542705527056270572705827059270602706127062270632706427065270662706727068270692707027071270722707327074270752707627077270782707927080270812708227083270842708527086270872708827089270902709127092270932709427095270962709727098270992710027101271022710327104271052710627107271082710927110271112711227113271142711527116271172711827119271202712127122271232712427125271262712727128271292713027131271322713327134271352713627137271382713927140271412714227143271442714527146271472714827149271502715127152271532715427155271562715727158271592716027161271622716327164271652716627167271682716927170271712717227173271742717527176271772717827179271802718127182271832718427185271862718727188271892719027191271922719327194271952719627197271982719927200272012720227203272042720527206272072720827209272102721127212272132721427215272162721727218272192722027221272222722327224272252722627227272282722927230272312723227233272342723527236272372723827239272402724127242272432724427245272462724727248272492725027251272522725327254272552725627257272582725927260272612726227263272642726527266272672726827269272702727127272272732727427275272762727727278272792728027281272822728327284272852728627287272882728927290272912729227293272942729527296272972729827299273002730127302273032730427305273062730727308273092731027311273122731327314273152731627317273182731927320273212732227323273242732527326273272732827329273302733127332273332733427335273362733727338273392734027341273422734327344273452734627347273482734927350273512735227353273542735527356273572735827359273602736127362273632736427365273662736727368273692737027371273722737327374273752737627377273782737927380273812738227383273842738527386273872738827389273902739127392273932739427395273962739727398273992740027401274022740327404274052740627407274082740927410274112741227413274142741527416274172741827419274202742127422274232742427425274262742727428274292743027431274322743327434274352743627437274382743927440274412744227443274442744527446274472744827449274502745127452274532745427455274562745727458274592746027461274622746327464274652746627467274682746927470274712747227473274742747527476274772747827479274802748127482274832748427485274862748727488274892749027491274922749327494274952749627497274982749927500275012750227503275042750527506275072750827509275102751127512275132751427515275162751727518275192752027521275222752327524275252752627527275282752927530275312753227533275342753527536275372753827539275402754127542275432754427545275462754727548275492755027551275522755327554275552755627557275582755927560275612756227563275642756527566275672756827569275702757127572275732757427575275762757727578275792758027581275822758327584275852758627587275882758927590275912759227593275942759527596275972759827599276002760127602276032760427605276062760727608276092761027611276122761327614276152761627617276182761927620276212762227623276242762527626276272762827629276302763127632276332763427635276362763727638276392764027641276422764327644276452764627647276482764927650276512765227653276542765527656276572765827659276602766127662276632766427665276662766727668276692767027671276722767327674276752767627677276782767927680276812768227683276842768527686276872768827689276902769127692276932769427695276962769727698276992770027701277022770327704277052770627707277082770927710277112771227713277142771527716277172771827719277202772127722277232772427725277262772727728277292773027731277322773327734277352773627737277382773927740277412774227743277442774527746277472774827749277502775127752277532775427755277562775727758277592776027761277622776327764277652776627767277682776927770277712777227773277742777527776277772777827779277802778127782277832778427785277862778727788277892779027791277922779327794277952779627797277982779927800278012780227803278042780527806278072780827809278102781127812278132781427815278162781727818278192782027821278222782327824278252782627827278282782927830278312783227833278342783527836278372783827839278402784127842278432784427845278462784727848278492785027851278522785327854278552785627857278582785927860278612786227863278642786527866278672786827869278702787127872278732787427875278762787727878278792788027881278822788327884278852788627887278882788927890278912789227893278942789527896278972789827899279002790127902279032790427905279062790727908279092791027911279122791327914279152791627917279182791927920279212792227923279242792527926279272792827929279302793127932279332793427935279362793727938279392794027941279422794327944279452794627947279482794927950279512795227953279542795527956279572795827959279602796127962279632796427965279662796727968279692797027971279722797327974279752797627977279782797927980279812798227983279842798527986279872798827989279902799127992279932799427995279962799727998279992800028001280022800328004280052800628007280082800928010280112801228013280142801528016280172801828019280202802128022280232802428025280262802728028280292803028031280322803328034280352803628037280382803928040280412804228043280442804528046280472804828049280502805128052280532805428055280562805728058280592806028061280622806328064280652806628067280682806928070280712807228073280742807528076280772807828079280802808128082280832808428085280862808728088280892809028091280922809328094280952809628097280982809928100281012810228103281042810528106281072810828109281102811128112281132811428115281162811728118281192812028121281222812328124281252812628127281282812928130281312813228133281342813528136281372813828139281402814128142281432814428145281462814728148281492815028151281522815328154281552815628157281582815928160281612816228163281642816528166281672816828169281702817128172281732817428175281762817728178281792818028181281822818328184281852818628187281882818928190281912819228193281942819528196281972819828199282002820128202282032820428205282062820728208282092821028211282122821328214282152821628217282182821928220282212822228223282242822528226282272822828229282302823128232282332823428235282362823728238282392824028241282422824328244282452824628247282482824928250282512825228253282542825528256282572825828259282602826128262282632826428265282662826728268282692827028271282722827328274282752827628277282782827928280282812828228283282842828528286282872828828289282902829128292282932829428295282962829728298282992830028301283022830328304283052830628307283082830928310283112831228313283142831528316283172831828319283202832128322283232832428325283262832728328283292833028331283322833328334283352833628337283382833928340283412834228343283442834528346283472834828349283502835128352283532835428355283562835728358283592836028361283622836328364283652836628367283682836928370283712837228373283742837528376283772837828379283802838128382283832838428385283862838728388283892839028391283922839328394283952839628397283982839928400284012840228403284042840528406284072840828409284102841128412284132841428415284162841728418284192842028421284222842328424284252842628427284282842928430284312843228433284342843528436284372843828439284402844128442284432844428445284462844728448284492845028451284522845328454284552845628457284582845928460284612846228463284642846528466284672846828469284702847128472284732847428475284762847728478284792848028481284822848328484284852848628487284882848928490284912849228493284942849528496284972849828499285002850128502285032850428505285062850728508285092851028511285122851328514285152851628517285182851928520285212852228523285242852528526285272852828529285302853128532285332853428535285362853728538285392854028541285422854328544285452854628547285482854928550285512855228553285542855528556285572855828559285602856128562285632856428565285662856728568285692857028571285722857328574285752857628577285782857928580285812858228583285842858528586285872858828589285902859128592285932859428595285962859728598285992860028601286022860328604286052860628607286082860928610286112861228613286142861528616286172861828619286202862128622286232862428625286262862728628286292863028631286322863328634286352863628637286382863928640286412864228643286442864528646286472864828649286502865128652286532865428655286562865728658286592866028661286622866328664286652866628667286682866928670286712867228673286742867528676286772867828679286802868128682286832868428685286862868728688286892869028691286922869328694286952869628697286982869928700287012870228703287042870528706287072870828709287102871128712287132871428715287162871728718287192872028721287222872328724287252872628727287282872928730287312873228733287342873528736287372873828739287402874128742287432874428745287462874728748287492875028751287522875328754287552875628757287582875928760287612876228763287642876528766287672876828769287702877128772287732877428775287762877728778287792878028781287822878328784287852878628787287882878928790287912879228793287942879528796287972879828799288002880128802288032880428805288062880728808288092881028811288122881328814288152881628817288182881928820288212882228823288242882528826288272882828829288302883128832288332883428835288362883728838288392884028841288422884328844288452884628847288482884928850288512885228853288542885528856288572885828859288602886128862288632886428865288662886728868288692887028871288722887328874288752887628877288782887928880288812888228883288842888528886288872888828889288902889128892288932889428895288962889728898288992890028901289022890328904289052890628907289082890928910289112891228913289142891528916289172891828919289202892128922289232892428925289262892728928289292893028931289322893328934289352893628937289382893928940289412894228943289442894528946289472894828949289502895128952289532895428955289562895728958289592896028961289622896328964289652896628967289682896928970289712897228973289742897528976289772897828979289802898128982289832898428985289862898728988289892899028991289922899328994289952899628997289982899929000290012900229003290042900529006290072900829009290102901129012290132901429015290162901729018290192902029021290222902329024290252902629027290282902929030290312903229033290342903529036290372903829039290402904129042290432904429045290462904729048290492905029051290522905329054290552905629057290582905929060290612906229063290642906529066290672906829069290702907129072290732907429075290762907729078290792908029081290822908329084290852908629087290882908929090290912909229093290942909529096290972909829099291002910129102291032910429105291062910729108291092911029111291122911329114291152911629117291182911929120291212912229123291242912529126291272912829129291302913129132291332913429135291362913729138291392914029141291422914329144291452914629147291482914929150291512915229153291542915529156291572915829159291602916129162291632916429165291662916729168291692917029171291722917329174291752917629177291782917929180291812918229183291842918529186291872918829189291902919129192291932919429195291962919729198291992920029201292022920329204292052920629207292082920929210292112921229213292142921529216292172921829219292202922129222292232922429225292262922729228292292923029231292322923329234292352923629237292382923929240292412924229243292442924529246292472924829249292502925129252292532925429255292562925729258292592926029261292622926329264292652926629267292682926929270292712927229273292742927529276292772927829279292802928129282292832928429285292862928729288292892929029291292922929329294292952929629297292982929929300293012930229303293042930529306293072930829309293102931129312293132931429315293162931729318293192932029321293222932329324293252932629327293282932929330293312933229333293342933529336293372933829339293402934129342293432934429345293462934729348293492935029351293522935329354293552935629357293582935929360293612936229363293642936529366293672936829369293702937129372293732937429375293762937729378293792938029381293822938329384293852938629387293882938929390293912939229393293942939529396293972939829399294002940129402294032940429405294062940729408294092941029411294122941329414294152941629417294182941929420294212942229423294242942529426294272942829429294302943129432294332943429435294362943729438294392944029441294422944329444294452944629447294482944929450294512945229453294542945529456294572945829459294602946129462294632946429465294662946729468294692947029471294722947329474294752947629477294782947929480294812948229483294842948529486294872948829489294902949129492294932949429495294962949729498294992950029501295022950329504295052950629507295082950929510295112951229513295142951529516295172951829519295202952129522295232952429525295262952729528295292953029531295322953329534295352953629537295382953929540295412954229543295442954529546295472954829549295502955129552295532955429555295562955729558295592956029561295622956329564295652956629567295682956929570295712957229573295742957529576295772957829579295802958129582295832958429585295862958729588295892959029591295922959329594295952959629597295982959929600296012960229603296042960529606296072960829609296102961129612296132961429615296162961729618296192962029621296222962329624296252962629627296282962929630296312963229633296342963529636296372963829639296402964129642296432964429645296462964729648296492965029651296522965329654296552965629657296582965929660296612966229663296642966529666296672966829669296702967129672296732967429675296762967729678296792968029681296822968329684296852968629687296882968929690296912969229693296942969529696296972969829699297002970129702297032970429705297062970729708297092971029711297122971329714297152971629717297182971929720297212972229723297242972529726297272972829729297302973129732297332973429735297362973729738297392974029741297422974329744297452974629747297482974929750297512975229753297542975529756297572975829759297602976129762297632976429765297662976729768297692977029771297722977329774297752977629777297782977929780297812978229783297842978529786297872978829789297902979129792297932979429795297962979729798297992980029801298022980329804298052980629807298082980929810298112981229813298142981529816298172981829819298202982129822298232982429825298262982729828298292983029831298322983329834298352983629837298382983929840298412984229843298442984529846298472984829849298502985129852298532985429855298562985729858298592986029861298622986329864298652986629867298682986929870298712987229873298742987529876298772987829879298802988129882298832988429885298862988729888298892989029891298922989329894298952989629897298982989929900299012990229903299042990529906299072990829909299102991129912299132991429915299162991729918299192992029921299222992329924299252992629927299282992929930299312993229933299342993529936299372993829939299402994129942299432994429945299462994729948299492995029951299522995329954299552995629957299582995929960299612996229963299642996529966299672996829969299702997129972299732997429975299762997729978299792998029981299822998329984299852998629987299882998929990299912999229993299942999529996299972999829999300003000130002300033000430005300063000730008300093001030011300123001330014300153001630017300183001930020300213002230023300243002530026300273002830029300303003130032300333003430035300363003730038300393004030041300423004330044300453004630047300483004930050300513005230053300543005530056300573005830059300603006130062300633006430065300663006730068300693007030071300723007330074300753007630077300783007930080300813008230083300843008530086300873008830089300903009130092300933009430095300963009730098300993010030101301023010330104301053010630107301083010930110301113011230113301143011530116301173011830119301203012130122301233012430125301263012730128301293013030131301323013330134301353013630137301383013930140301413014230143301443014530146301473014830149301503015130152301533015430155301563015730158301593016030161301623016330164301653016630167301683016930170301713017230173301743017530176301773017830179301803018130182301833018430185301863018730188301893019030191301923019330194301953019630197301983019930200302013020230203302043020530206302073020830209302103021130212302133021430215302163021730218302193022030221302223022330224302253022630227302283022930230302313023230233302343023530236302373023830239302403024130242302433024430245302463024730248302493025030251302523025330254302553025630257302583025930260302613026230263302643026530266302673026830269302703027130272302733027430275302763027730278302793028030281302823028330284302853028630287302883028930290302913029230293302943029530296302973029830299303003030130302303033030430305303063030730308303093031030311303123031330314303153031630317303183031930320303213032230323303243032530326303273032830329303303033130332303333033430335303363033730338303393034030341303423034330344303453034630347303483034930350303513035230353303543035530356303573035830359303603036130362303633036430365303663036730368303693037030371303723037330374303753037630377303783037930380303813038230383303843038530386303873038830389303903039130392303933039430395303963039730398303993040030401304023040330404304053040630407304083040930410304113041230413304143041530416304173041830419304203042130422304233042430425304263042730428304293043030431304323043330434304353043630437304383043930440304413044230443304443044530446304473044830449304503045130452304533045430455304563045730458304593046030461304623046330464304653046630467304683046930470304713047230473304743047530476304773047830479304803048130482304833048430485304863048730488304893049030491304923049330494304953049630497304983049930500305013050230503305043050530506305073050830509305103051130512305133051430515305163051730518305193052030521305223052330524305253052630527305283052930530305313053230533305343053530536305373053830539305403054130542305433054430545305463054730548305493055030551305523055330554305553055630557305583055930560305613056230563305643056530566305673056830569305703057130572305733057430575305763057730578305793058030581305823058330584305853058630587305883058930590305913059230593305943059530596305973059830599306003060130602306033060430605306063060730608306093061030611306123061330614306153061630617306183061930620306213062230623306243062530626306273062830629306303063130632306333063430635306363063730638306393064030641306423064330644306453064630647306483064930650306513065230653306543065530656306573065830659306603066130662306633066430665306663066730668306693067030671306723067330674306753067630677306783067930680306813068230683306843068530686306873068830689306903069130692306933069430695306963069730698306993070030701307023070330704307053070630707307083070930710307113071230713307143071530716307173071830719307203072130722307233072430725307263072730728307293073030731307323073330734307353073630737307383073930740307413074230743307443074530746307473074830749307503075130752307533075430755307563075730758307593076030761307623076330764307653076630767307683076930770307713077230773307743077530776307773077830779307803078130782307833078430785307863078730788307893079030791307923079330794307953079630797307983079930800308013080230803308043080530806308073080830809308103081130812308133081430815308163081730818308193082030821308223082330824308253082630827308283082930830308313083230833308343083530836308373083830839308403084130842308433084430845308463084730848308493085030851308523085330854308553085630857308583085930860308613086230863308643086530866308673086830869308703087130872308733087430875308763087730878308793088030881308823088330884308853088630887308883088930890308913089230893308943089530896308973089830899309003090130902309033090430905309063090730908309093091030911309123091330914309153091630917309183091930920309213092230923309243092530926309273092830929309303093130932309333093430935309363093730938309393094030941309423094330944309453094630947309483094930950309513095230953309543095530956309573095830959309603096130962309633096430965309663096730968309693097030971309723097330974309753097630977309783097930980309813098230983309843098530986309873098830989309903099130992309933099430995309963099730998309993100031001310023100331004310053100631007310083100931010310113101231013310143101531016310173101831019310203102131022310233102431025310263102731028310293103031031310323103331034310353103631037310383103931040310413104231043310443104531046310473104831049310503105131052310533105431055310563105731058310593106031061310623106331064310653106631067310683106931070310713107231073310743107531076310773107831079310803108131082310833108431085310863108731088310893109031091310923109331094310953109631097310983109931100311013110231103311043110531106311073110831109311103111131112311133111431115311163111731118311193112031121311223112331124311253112631127311283112931130311313113231133311343113531136311373113831139311403114131142311433114431145311463114731148311493115031151311523115331154311553115631157311583115931160311613116231163311643116531166311673116831169311703117131172311733117431175311763117731178311793118031181311823118331184311853118631187311883118931190311913119231193311943119531196311973119831199312003120131202312033120431205312063120731208312093121031211312123121331214312153121631217312183121931220312213122231223312243122531226312273122831229312303123131232312333123431235312363123731238312393124031241312423124331244312453124631247312483124931250312513125231253312543125531256312573125831259312603126131262312633126431265312663126731268312693127031271312723127331274312753127631277312783127931280312813128231283312843128531286312873128831289312903129131292312933129431295312963129731298312993130031301313023130331304313053130631307313083130931310313113131231313313143131531316313173131831319313203132131322313233132431325313263132731328313293133031331313323133331334313353133631337313383133931340313413134231343313443134531346313473134831349313503135131352313533135431355313563135731358313593136031361313623136331364313653136631367313683136931370313713137231373313743137531376313773137831379313803138131382313833138431385313863138731388313893139031391313923139331394313953139631397313983139931400314013140231403314043140531406314073140831409314103141131412314133141431415314163141731418314193142031421314223142331424314253142631427314283142931430314313143231433314343143531436314373143831439314403144131442314433144431445314463144731448314493145031451314523145331454314553145631457314583145931460314613146231463314643146531466314673146831469314703147131472314733147431475314763147731478314793148031481314823148331484314853148631487314883148931490314913149231493314943149531496314973149831499315003150131502315033150431505315063150731508315093151031511315123151331514315153151631517315183151931520315213152231523315243152531526315273152831529315303153131532315333153431535315363153731538315393154031541315423154331544315453154631547315483154931550315513155231553315543155531556315573155831559315603156131562315633156431565315663156731568315693157031571315723157331574315753157631577315783157931580315813158231583315843158531586315873158831589315903159131592315933159431595315963159731598315993160031601316023160331604316053160631607316083160931610316113161231613316143161531616316173161831619316203162131622316233162431625316263162731628316293163031631316323163331634316353163631637316383163931640316413164231643316443164531646316473164831649316503165131652316533165431655316563165731658316593166031661316623166331664316653166631667316683166931670316713167231673316743167531676316773167831679316803168131682316833168431685316863168731688316893169031691316923169331694316953169631697316983169931700317013170231703317043170531706317073170831709317103171131712317133171431715317163171731718317193172031721317223172331724317253172631727317283172931730317313173231733317343173531736317373173831739317403174131742317433174431745317463174731748317493175031751317523175331754317553175631757317583175931760317613176231763317643176531766317673176831769317703177131772317733177431775317763177731778317793178031781317823178331784317853178631787317883178931790317913179231793317943179531796 |
- 'use strict';
- Object.defineProperty(exports, '__esModule', { value: true });
- var app = require('@firebase/app');
- var component = require('@firebase/component');
- var logger = require('@firebase/logger');
- var util$1 = require('util');
- var util = require('@firebase/util');
- var crypto = require('crypto');
- var webchannelWrapper = require('@firebase/webchannel-wrapper');
- var grpc = require('@grpc/grpc-js');
- var protoLoader = require('@grpc/proto-loader');
- function _interopNamespace(e) {
- if (e && e.__esModule) return e;
- var n = Object.create(null);
- if (e) {
- Object.keys(e).forEach(function (k) {
- if (k !== 'default') {
- var d = Object.getOwnPropertyDescriptor(e, k);
- Object.defineProperty(n, k, d.get ? d : {
- enumerable: true,
- get: function () { return e[k]; }
- });
- }
- });
- }
- n["default"] = e;
- return Object.freeze(n);
- }
- var grpc__namespace = /*#__PURE__*/_interopNamespace(grpc);
- var protoLoader__namespace = /*#__PURE__*/_interopNamespace(protoLoader);
- const name = "@firebase/firestore";
- const version$1 = "3.13.0";
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Simple wrapper around a nullable UID. Mostly exists to make code more
- * readable.
- */
- class User {
- constructor(uid) {
- this.uid = uid;
- }
- isAuthenticated() {
- return this.uid != null;
- }
- /**
- * Returns a key representing this user, suitable for inclusion in a
- * dictionary.
- */
- toKey() {
- if (this.isAuthenticated()) {
- return 'uid:' + this.uid;
- }
- else {
- return 'anonymous-user';
- }
- }
- isEqual(otherUser) {
- return otherUser.uid === this.uid;
- }
- }
- /** A user with a null UID. */
- User.UNAUTHENTICATED = new User(null);
- // TODO(mikelehen): Look into getting a proper uid-equivalent for
- // non-FirebaseAuth providers.
- User.GOOGLE_CREDENTIALS = new User('google-credentials-uid');
- User.FIRST_PARTY = new User('first-party-uid');
- User.MOCK_USER = new User('mock-user');
- const version = "9.23.0";
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- let SDK_VERSION = version;
- function setSDKVersion(version) {
- SDK_VERSION = version;
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /** Formats an object as a JSON string, suitable for logging. */
- function formatJSON(value) {
- // util.inspect() results in much more readable output than JSON.stringify()
- return util$1.inspect(value, { depth: 100 });
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const logClient = new logger.Logger('@firebase/firestore');
- // Helper methods are needed because variables can't be exported as read/write
- function getLogLevel() {
- return logClient.logLevel;
- }
- /**
- * Sets the verbosity of Cloud Firestore logs (debug, error, or silent).
- *
- * @param logLevel - The verbosity you set for activity and error logging. Can
- * be any of the following values:
- *
- * <ul>
- * <li>`debug` for the most verbose logging level, primarily for
- * debugging.</li>
- * <li>`error` to log errors only.</li>
- * <li><code>`silent` to turn off logging.</li>
- * </ul>
- */
- function setLogLevel(logLevel) {
- logClient.setLogLevel(logLevel);
- }
- function logDebug(msg, ...obj) {
- if (logClient.logLevel <= logger.LogLevel.DEBUG) {
- const args = obj.map(argToString);
- logClient.debug(`Firestore (${SDK_VERSION}): ${msg}`, ...args);
- }
- }
- function logError(msg, ...obj) {
- if (logClient.logLevel <= logger.LogLevel.ERROR) {
- const args = obj.map(argToString);
- logClient.error(`Firestore (${SDK_VERSION}): ${msg}`, ...args);
- }
- }
- /**
- * @internal
- */
- function logWarn(msg, ...obj) {
- if (logClient.logLevel <= logger.LogLevel.WARN) {
- const args = obj.map(argToString);
- logClient.warn(`Firestore (${SDK_VERSION}): ${msg}`, ...args);
- }
- }
- /**
- * Converts an additional log parameter to a string representation.
- */
- function argToString(obj) {
- if (typeof obj === 'string') {
- return obj;
- }
- else {
- try {
- return formatJSON(obj);
- }
- catch (e) {
- // Converting to JSON failed, just log the object directly
- return obj;
- }
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Unconditionally fails, throwing an Error with the given message.
- * Messages are stripped in production builds.
- *
- * Returns `never` and can be used in expressions:
- * @example
- * let futureVar = fail('not implemented yet');
- */
- function fail(failure = 'Unexpected state') {
- // Log the failure in addition to throw an exception, just in case the
- // exception is swallowed.
- const message = `FIRESTORE (${SDK_VERSION}) INTERNAL ASSERTION FAILED: ` + failure;
- logError(message);
- // NOTE: We don't use FirestoreError here because these are internal failures
- // that cannot be handled by the user. (Also it would create a circular
- // dependency between the error and assert modules which doesn't work.)
- throw new Error(message);
- }
- /**
- * Fails if the given assertion condition is false, throwing an Error with the
- * given message if it did.
- *
- * Messages are stripped in production builds.
- */
- function hardAssert(assertion, message) {
- if (!assertion) {
- fail();
- }
- }
- /**
- * Fails if the given assertion condition is false, throwing an Error with the
- * given message if it did.
- *
- * The code of callsites invoking this function are stripped out in production
- * builds. Any side-effects of code within the debugAssert() invocation will not
- * happen in this case.
- *
- * @internal
- */
- function debugAssert(assertion, message) {
- if (!assertion) {
- fail();
- }
- }
- /**
- * Casts `obj` to `T`. In non-production builds, verifies that `obj` is an
- * instance of `T` before casting.
- */
- function debugCast(obj,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- constructor) {
- return obj;
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const Code = {
- // Causes are copied from:
- // https://github.com/grpc/grpc/blob/bceec94ea4fc5f0085d81235d8e1c06798dc341a/include/grpc%2B%2B/impl/codegen/status_code_enum.h
- /** Not an error; returned on success. */
- OK: 'ok',
- /** The operation was cancelled (typically by the caller). */
- CANCELLED: 'cancelled',
- /** Unknown error or an error from a different error domain. */
- UNKNOWN: 'unknown',
- /**
- * Client specified an invalid argument. Note that this differs from
- * FAILED_PRECONDITION. INVALID_ARGUMENT indicates arguments that are
- * problematic regardless of the state of the system (e.g., a malformed file
- * name).
- */
- INVALID_ARGUMENT: 'invalid-argument',
- /**
- * Deadline expired before operation could complete. For operations that
- * change the state of the system, this error may be returned even if the
- * operation has completed successfully. For example, a successful response
- * from a server could have been delayed long enough for the deadline to
- * expire.
- */
- DEADLINE_EXCEEDED: 'deadline-exceeded',
- /** Some requested entity (e.g., file or directory) was not found. */
- NOT_FOUND: 'not-found',
- /**
- * Some entity that we attempted to create (e.g., file or directory) already
- * exists.
- */
- ALREADY_EXISTS: 'already-exists',
- /**
- * The caller does not have permission to execute the specified operation.
- * PERMISSION_DENIED must not be used for rejections caused by exhausting
- * some resource (use RESOURCE_EXHAUSTED instead for those errors).
- * PERMISSION_DENIED must not be used if the caller can not be identified
- * (use UNAUTHENTICATED instead for those errors).
- */
- PERMISSION_DENIED: 'permission-denied',
- /**
- * The request does not have valid authentication credentials for the
- * operation.
- */
- UNAUTHENTICATED: 'unauthenticated',
- /**
- * Some resource has been exhausted, perhaps a per-user quota, or perhaps the
- * entire file system is out of space.
- */
- RESOURCE_EXHAUSTED: 'resource-exhausted',
- /**
- * Operation was rejected because the system is not in a state required for
- * the operation's execution. For example, directory to be deleted may be
- * non-empty, an rmdir operation is applied to a non-directory, etc.
- *
- * A litmus test that may help a service implementor in deciding
- * between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE:
- * (a) Use UNAVAILABLE if the client can retry just the failing call.
- * (b) Use ABORTED if the client should retry at a higher-level
- * (e.g., restarting a read-modify-write sequence).
- * (c) Use FAILED_PRECONDITION if the client should not retry until
- * the system state has been explicitly fixed. E.g., if an "rmdir"
- * fails because the directory is non-empty, FAILED_PRECONDITION
- * should be returned since the client should not retry unless
- * they have first fixed up the directory by deleting files from it.
- * (d) Use FAILED_PRECONDITION if the client performs conditional
- * REST Get/Update/Delete on a resource and the resource on the
- * server does not match the condition. E.g., conflicting
- * read-modify-write on the same resource.
- */
- FAILED_PRECONDITION: 'failed-precondition',
- /**
- * The operation was aborted, typically due to a concurrency issue like
- * sequencer check failures, transaction aborts, etc.
- *
- * See litmus test above for deciding between FAILED_PRECONDITION, ABORTED,
- * and UNAVAILABLE.
- */
- ABORTED: 'aborted',
- /**
- * Operation was attempted past the valid range. E.g., seeking or reading
- * past end of file.
- *
- * Unlike INVALID_ARGUMENT, this error indicates a problem that may be fixed
- * if the system state changes. For example, a 32-bit file system will
- * generate INVALID_ARGUMENT if asked to read at an offset that is not in the
- * range [0,2^32-1], but it will generate OUT_OF_RANGE if asked to read from
- * an offset past the current file size.
- *
- * There is a fair bit of overlap between FAILED_PRECONDITION and
- * OUT_OF_RANGE. We recommend using OUT_OF_RANGE (the more specific error)
- * when it applies so that callers who are iterating through a space can
- * easily look for an OUT_OF_RANGE error to detect when they are done.
- */
- OUT_OF_RANGE: 'out-of-range',
- /** Operation is not implemented or not supported/enabled in this service. */
- UNIMPLEMENTED: 'unimplemented',
- /**
- * Internal errors. Means some invariants expected by underlying System has
- * been broken. If you see one of these errors, Something is very broken.
- */
- INTERNAL: 'internal',
- /**
- * The service is currently unavailable. This is a most likely a transient
- * condition and may be corrected by retrying with a backoff.
- *
- * See litmus test above for deciding between FAILED_PRECONDITION, ABORTED,
- * and UNAVAILABLE.
- */
- UNAVAILABLE: 'unavailable',
- /** Unrecoverable data loss or corruption. */
- DATA_LOSS: 'data-loss'
- };
- /** An error returned by a Firestore operation. */
- class FirestoreError extends util.FirebaseError {
- /** @hideconstructor */
- constructor(
- /**
- * The backend error code associated with this error.
- */
- code,
- /**
- * A custom error description.
- */
- message) {
- super(code, message);
- this.code = code;
- this.message = message;
- // HACK: We write a toString property directly because Error is not a real
- // class and so inheritance does not work correctly. We could alternatively
- // do the same "back-door inheritance" trick that FirebaseError does.
- this.toString = () => `${this.name}: [code=${this.code}]: ${this.message}`;
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- class Deferred {
- constructor() {
- this.promise = new Promise((resolve, reject) => {
- this.resolve = resolve;
- this.reject = reject;
- });
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- class OAuthToken {
- constructor(value, user) {
- this.user = user;
- this.type = 'OAuth';
- this.headers = new Map();
- this.headers.set('Authorization', `Bearer ${value}`);
- }
- }
- /**
- * A CredentialsProvider that always yields an empty token.
- * @internal
- */
- class EmptyAuthCredentialsProvider {
- getToken() {
- return Promise.resolve(null);
- }
- invalidateToken() { }
- start(asyncQueue, changeListener) {
- // Fire with initial user.
- asyncQueue.enqueueRetryable(() => changeListener(User.UNAUTHENTICATED));
- }
- shutdown() { }
- }
- /**
- * A CredentialsProvider that always returns a constant token. Used for
- * emulator token mocking.
- */
- class EmulatorAuthCredentialsProvider {
- constructor(token) {
- this.token = token;
- /**
- * Stores the listener registered with setChangeListener()
- * This isn't actually necessary since the UID never changes, but we use this
- * to verify the listen contract is adhered to in tests.
- */
- this.changeListener = null;
- }
- getToken() {
- return Promise.resolve(this.token);
- }
- invalidateToken() { }
- start(asyncQueue, changeListener) {
- this.changeListener = changeListener;
- // Fire with initial user.
- asyncQueue.enqueueRetryable(() => changeListener(this.token.user));
- }
- shutdown() {
- this.changeListener = null;
- }
- }
- class FirebaseAuthCredentialsProvider {
- constructor(authProvider) {
- this.authProvider = authProvider;
- /** Tracks the current User. */
- this.currentUser = User.UNAUTHENTICATED;
- /**
- * Counter used to detect if the token changed while a getToken request was
- * outstanding.
- */
- this.tokenCounter = 0;
- this.forceRefresh = false;
- this.auth = null;
- }
- start(asyncQueue, changeListener) {
- let lastTokenId = this.tokenCounter;
- // A change listener that prevents double-firing for the same token change.
- const guardedChangeListener = user => {
- if (this.tokenCounter !== lastTokenId) {
- lastTokenId = this.tokenCounter;
- return changeListener(user);
- }
- else {
- return Promise.resolve();
- }
- };
- // A promise that can be waited on to block on the next token change.
- // This promise is re-created after each change.
- let nextToken = new Deferred();
- this.tokenListener = () => {
- this.tokenCounter++;
- this.currentUser = this.getUser();
- nextToken.resolve();
- nextToken = new Deferred();
- asyncQueue.enqueueRetryable(() => guardedChangeListener(this.currentUser));
- };
- const awaitNextToken = () => {
- const currentTokenAttempt = nextToken;
- asyncQueue.enqueueRetryable(async () => {
- await currentTokenAttempt.promise;
- await guardedChangeListener(this.currentUser);
- });
- };
- const registerAuth = (auth) => {
- logDebug('FirebaseAuthCredentialsProvider', 'Auth detected');
- this.auth = auth;
- this.auth.addAuthTokenListener(this.tokenListener);
- awaitNextToken();
- };
- this.authProvider.onInit(auth => registerAuth(auth));
- // Our users can initialize Auth right after Firestore, so we give it
- // a chance to register itself with the component framework before we
- // determine whether to start up in unauthenticated mode.
- setTimeout(() => {
- if (!this.auth) {
- const auth = this.authProvider.getImmediate({ optional: true });
- if (auth) {
- registerAuth(auth);
- }
- else {
- // If auth is still not available, proceed with `null` user
- logDebug('FirebaseAuthCredentialsProvider', 'Auth not yet detected');
- nextToken.resolve();
- nextToken = new Deferred();
- }
- }
- }, 0);
- awaitNextToken();
- }
- getToken() {
- // Take note of the current value of the tokenCounter so that this method
- // can fail (with an ABORTED error) if there is a token change while the
- // request is outstanding.
- const initialTokenCounter = this.tokenCounter;
- const forceRefresh = this.forceRefresh;
- this.forceRefresh = false;
- if (!this.auth) {
- return Promise.resolve(null);
- }
- return this.auth.getToken(forceRefresh).then(tokenData => {
- // Cancel the request since the token changed while the request was
- // outstanding so the response is potentially for a previous user (which
- // user, we can't be sure).
- if (this.tokenCounter !== initialTokenCounter) {
- logDebug('FirebaseAuthCredentialsProvider', 'getToken aborted due to token change.');
- return this.getToken();
- }
- else {
- if (tokenData) {
- hardAssert(typeof tokenData.accessToken === 'string');
- return new OAuthToken(tokenData.accessToken, this.currentUser);
- }
- else {
- return null;
- }
- }
- });
- }
- invalidateToken() {
- this.forceRefresh = true;
- }
- shutdown() {
- if (this.auth) {
- this.auth.removeAuthTokenListener(this.tokenListener);
- }
- }
- // Auth.getUid() can return null even with a user logged in. It is because
- // getUid() is synchronous, but the auth code populating Uid is asynchronous.
- // This method should only be called in the AuthTokenListener callback
- // to guarantee to get the actual user.
- getUser() {
- const currentUid = this.auth && this.auth.getUid();
- hardAssert(currentUid === null || typeof currentUid === 'string');
- return new User(currentUid);
- }
- }
- /*
- * FirstPartyToken provides a fresh token each time its value
- * is requested, because if the token is too old, requests will be rejected.
- * Technically this may no longer be necessary since the SDK should gracefully
- * recover from unauthenticated errors (see b/33147818 for context), but it's
- * safer to keep the implementation as-is.
- */
- class FirstPartyToken {
- constructor(sessionIndex, iamToken, authTokenFactory) {
- this.sessionIndex = sessionIndex;
- this.iamToken = iamToken;
- this.authTokenFactory = authTokenFactory;
- this.type = 'FirstParty';
- this.user = User.FIRST_PARTY;
- this._headers = new Map();
- }
- /**
- * Gets an authorization token, using a provided factory function, or return
- * null.
- */
- getAuthToken() {
- if (this.authTokenFactory) {
- return this.authTokenFactory();
- }
- else {
- return null;
- }
- }
- get headers() {
- this._headers.set('X-Goog-AuthUser', this.sessionIndex);
- // Use array notation to prevent minification
- const authHeaderTokenValue = this.getAuthToken();
- if (authHeaderTokenValue) {
- this._headers.set('Authorization', authHeaderTokenValue);
- }
- if (this.iamToken) {
- this._headers.set('X-Goog-Iam-Authorization-Token', this.iamToken);
- }
- return this._headers;
- }
- }
- /*
- * Provides user credentials required for the Firestore JavaScript SDK
- * to authenticate the user, using technique that is only available
- * to applications hosted by Google.
- */
- class FirstPartyAuthCredentialsProvider {
- constructor(sessionIndex, iamToken, authTokenFactory) {
- this.sessionIndex = sessionIndex;
- this.iamToken = iamToken;
- this.authTokenFactory = authTokenFactory;
- }
- getToken() {
- return Promise.resolve(new FirstPartyToken(this.sessionIndex, this.iamToken, this.authTokenFactory));
- }
- start(asyncQueue, changeListener) {
- // Fire with initial uid.
- asyncQueue.enqueueRetryable(() => changeListener(User.FIRST_PARTY));
- }
- shutdown() { }
- invalidateToken() { }
- }
- class AppCheckToken {
- constructor(value) {
- this.value = value;
- this.type = 'AppCheck';
- this.headers = new Map();
- if (value && value.length > 0) {
- this.headers.set('x-firebase-appcheck', this.value);
- }
- }
- }
- class FirebaseAppCheckTokenProvider {
- constructor(appCheckProvider) {
- this.appCheckProvider = appCheckProvider;
- this.forceRefresh = false;
- this.appCheck = null;
- this.latestAppCheckToken = null;
- }
- start(asyncQueue, changeListener) {
- const onTokenChanged = tokenResult => {
- if (tokenResult.error != null) {
- logDebug('FirebaseAppCheckTokenProvider', `Error getting App Check token; using placeholder token instead. Error: ${tokenResult.error.message}`);
- }
- const tokenUpdated = tokenResult.token !== this.latestAppCheckToken;
- this.latestAppCheckToken = tokenResult.token;
- logDebug('FirebaseAppCheckTokenProvider', `Received ${tokenUpdated ? 'new' : 'existing'} token.`);
- return tokenUpdated
- ? changeListener(tokenResult.token)
- : Promise.resolve();
- };
- this.tokenListener = (tokenResult) => {
- asyncQueue.enqueueRetryable(() => onTokenChanged(tokenResult));
- };
- const registerAppCheck = (appCheck) => {
- logDebug('FirebaseAppCheckTokenProvider', 'AppCheck detected');
- this.appCheck = appCheck;
- this.appCheck.addTokenListener(this.tokenListener);
- };
- this.appCheckProvider.onInit(appCheck => registerAppCheck(appCheck));
- // Our users can initialize AppCheck after Firestore, so we give it
- // a chance to register itself with the component framework.
- setTimeout(() => {
- if (!this.appCheck) {
- const appCheck = this.appCheckProvider.getImmediate({ optional: true });
- if (appCheck) {
- registerAppCheck(appCheck);
- }
- else {
- // If AppCheck is still not available, proceed without it.
- logDebug('FirebaseAppCheckTokenProvider', 'AppCheck not yet detected');
- }
- }
- }, 0);
- }
- getToken() {
- const forceRefresh = this.forceRefresh;
- this.forceRefresh = false;
- if (!this.appCheck) {
- return Promise.resolve(null);
- }
- return this.appCheck.getToken(forceRefresh).then(tokenResult => {
- if (tokenResult) {
- hardAssert(typeof tokenResult.token === 'string');
- this.latestAppCheckToken = tokenResult.token;
- return new AppCheckToken(tokenResult.token);
- }
- else {
- return null;
- }
- });
- }
- invalidateToken() {
- this.forceRefresh = true;
- }
- shutdown() {
- if (this.appCheck) {
- this.appCheck.removeTokenListener(this.tokenListener);
- }
- }
- }
- /**
- * An AppCheck token provider that always yields an empty token.
- * @internal
- */
- class EmptyAppCheckTokenProvider {
- getToken() {
- return Promise.resolve(new AppCheckToken(''));
- }
- invalidateToken() { }
- start(asyncQueue, changeListener) { }
- shutdown() { }
- }
- /**
- * Builds a CredentialsProvider depending on the type of
- * the credentials passed in.
- */
- function makeAuthCredentialsProvider(credentials) {
- if (!credentials) {
- return new EmptyAuthCredentialsProvider();
- }
- switch (credentials['type']) {
- case 'firstParty':
- return new FirstPartyAuthCredentialsProvider(credentials['sessionIndex'] || '0', credentials['iamToken'] || null, credentials['authTokenFactory'] || null);
- case 'provider':
- return credentials['client'];
- default:
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'makeAuthCredentialsProvider failed due to invalid credential type');
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Generates `nBytes` of random bytes.
- *
- * If `nBytes < 0` , an error will be thrown.
- */
- function randomBytes(nBytes) {
- return crypto.randomBytes(nBytes);
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- class AutoId {
- static newId() {
- // Alphanumeric characters
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
- // The largest byte value that is a multiple of `char.length`.
- const maxMultiple = Math.floor(256 / chars.length) * chars.length;
- let autoId = '';
- const targetLength = 20;
- while (autoId.length < targetLength) {
- const bytes = randomBytes(40);
- for (let i = 0; i < bytes.length; ++i) {
- // Only accept values that are [0, maxMultiple), this ensures they can
- // be evenly mapped to indices of `chars` via a modulo operation.
- if (autoId.length < targetLength && bytes[i] < maxMultiple) {
- autoId += chars.charAt(bytes[i] % chars.length);
- }
- }
- }
- return autoId;
- }
- }
- function primitiveComparator(left, right) {
- if (left < right) {
- return -1;
- }
- if (left > right) {
- return 1;
- }
- return 0;
- }
- /** Helper to compare arrays using isEqual(). */
- function arrayEquals(left, right, comparator) {
- if (left.length !== right.length) {
- return false;
- }
- return left.every((value, index) => comparator(value, right[index]));
- }
- /**
- * Returns the immediate lexicographically-following string. This is useful to
- * construct an inclusive range for indexeddb iterators.
- */
- function immediateSuccessor(s) {
- // Return the input string, with an additional NUL byte appended.
- return s + '\0';
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- // The earliest date supported by Firestore timestamps (0001-01-01T00:00:00Z).
- const MIN_SECONDS = -62135596800;
- // Number of nanoseconds in a millisecond.
- const MS_TO_NANOS = 1e6;
- /**
- * A `Timestamp` represents a point in time independent of any time zone or
- * calendar, represented as seconds and fractions of seconds at nanosecond
- * resolution in UTC Epoch time.
- *
- * It is encoded using the Proleptic Gregorian Calendar which extends the
- * Gregorian calendar backwards to year one. It is encoded assuming all minutes
- * are 60 seconds long, i.e. leap seconds are "smeared" so that no leap second
- * table is needed for interpretation. Range is from 0001-01-01T00:00:00Z to
- * 9999-12-31T23:59:59.999999999Z.
- *
- * For examples and further specifications, refer to the
- * {@link https://github.com/google/protobuf/blob/master/src/google/protobuf/timestamp.proto | Timestamp definition}.
- */
- class Timestamp {
- /**
- * Creates a new timestamp.
- *
- * @param seconds - The number of seconds of UTC time since Unix epoch
- * 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
- * 9999-12-31T23:59:59Z inclusive.
- * @param nanoseconds - The non-negative fractions of a second at nanosecond
- * resolution. Negative second values with fractions must still have
- * non-negative nanoseconds values that count forward in time. Must be
- * from 0 to 999,999,999 inclusive.
- */
- constructor(
- /**
- * The number of seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z.
- */
- seconds,
- /**
- * The fractions of a second at nanosecond resolution.*
- */
- nanoseconds) {
- this.seconds = seconds;
- this.nanoseconds = nanoseconds;
- if (nanoseconds < 0) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Timestamp nanoseconds out of range: ' + nanoseconds);
- }
- if (nanoseconds >= 1e9) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Timestamp nanoseconds out of range: ' + nanoseconds);
- }
- if (seconds < MIN_SECONDS) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Timestamp seconds out of range: ' + seconds);
- }
- // This will break in the year 10,000.
- if (seconds >= 253402300800) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Timestamp seconds out of range: ' + seconds);
- }
- }
- /**
- * Creates a new timestamp with the current date, with millisecond precision.
- *
- * @returns a new timestamp representing the current date.
- */
- static now() {
- return Timestamp.fromMillis(Date.now());
- }
- /**
- * Creates a new timestamp from the given date.
- *
- * @param date - The date to initialize the `Timestamp` from.
- * @returns A new `Timestamp` representing the same point in time as the given
- * date.
- */
- static fromDate(date) {
- return Timestamp.fromMillis(date.getTime());
- }
- /**
- * Creates a new timestamp from the given number of milliseconds.
- *
- * @param milliseconds - Number of milliseconds since Unix epoch
- * 1970-01-01T00:00:00Z.
- * @returns A new `Timestamp` representing the same point in time as the given
- * number of milliseconds.
- */
- static fromMillis(milliseconds) {
- const seconds = Math.floor(milliseconds / 1000);
- const nanos = Math.floor((milliseconds - seconds * 1000) * MS_TO_NANOS);
- return new Timestamp(seconds, nanos);
- }
- /**
- * Converts a `Timestamp` to a JavaScript `Date` object. This conversion
- * causes a loss of precision since `Date` objects only support millisecond
- * precision.
- *
- * @returns JavaScript `Date` object representing the same point in time as
- * this `Timestamp`, with millisecond precision.
- */
- toDate() {
- return new Date(this.toMillis());
- }
- /**
- * Converts a `Timestamp` to a numeric timestamp (in milliseconds since
- * epoch). This operation causes a loss of precision.
- *
- * @returns The point in time corresponding to this timestamp, represented as
- * the number of milliseconds since Unix epoch 1970-01-01T00:00:00Z.
- */
- toMillis() {
- return this.seconds * 1000 + this.nanoseconds / MS_TO_NANOS;
- }
- _compareTo(other) {
- if (this.seconds === other.seconds) {
- return primitiveComparator(this.nanoseconds, other.nanoseconds);
- }
- return primitiveComparator(this.seconds, other.seconds);
- }
- /**
- * Returns true if this `Timestamp` is equal to the provided one.
- *
- * @param other - The `Timestamp` to compare against.
- * @returns true if this `Timestamp` is equal to the provided one.
- */
- isEqual(other) {
- return (other.seconds === this.seconds && other.nanoseconds === this.nanoseconds);
- }
- /** Returns a textual representation of this `Timestamp`. */
- toString() {
- return ('Timestamp(seconds=' +
- this.seconds +
- ', nanoseconds=' +
- this.nanoseconds +
- ')');
- }
- /** Returns a JSON-serializable representation of this `Timestamp`. */
- toJSON() {
- return { seconds: this.seconds, nanoseconds: this.nanoseconds };
- }
- /**
- * Converts this object to a primitive string, which allows `Timestamp` objects
- * to be compared using the `>`, `<=`, `>=` and `>` operators.
- */
- valueOf() {
- // This method returns a string of the form <seconds>.<nanoseconds> where
- // <seconds> is translated to have a non-negative value and both <seconds>
- // and <nanoseconds> are left-padded with zeroes to be a consistent length.
- // Strings with this format then have a lexiographical ordering that matches
- // the expected ordering. The <seconds> translation is done to avoid having
- // a leading negative sign (i.e. a leading '-' character) in its string
- // representation, which would affect its lexiographical ordering.
- const adjustedSeconds = this.seconds - MIN_SECONDS;
- // Note: Up to 12 decimal digits are required to represent all valid
- // 'seconds' values.
- const formattedSeconds = String(adjustedSeconds).padStart(12, '0');
- const formattedNanoseconds = String(this.nanoseconds).padStart(9, '0');
- return formattedSeconds + '.' + formattedNanoseconds;
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * A version of a document in Firestore. This corresponds to the version
- * timestamp, such as update_time or read_time.
- */
- class SnapshotVersion {
- constructor(timestamp) {
- this.timestamp = timestamp;
- }
- static fromTimestamp(value) {
- return new SnapshotVersion(value);
- }
- static min() {
- return new SnapshotVersion(new Timestamp(0, 0));
- }
- static max() {
- return new SnapshotVersion(new Timestamp(253402300799, 1e9 - 1));
- }
- compareTo(other) {
- return this.timestamp._compareTo(other.timestamp);
- }
- isEqual(other) {
- return this.timestamp.isEqual(other.timestamp);
- }
- /** Returns a number representation of the version for use in spec tests. */
- toMicroseconds() {
- // Convert to microseconds.
- return this.timestamp.seconds * 1e6 + this.timestamp.nanoseconds / 1000;
- }
- toString() {
- return 'SnapshotVersion(' + this.timestamp.toString() + ')';
- }
- toTimestamp() {
- return this.timestamp;
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const DOCUMENT_KEY_NAME = '__name__';
- /**
- * Path represents an ordered sequence of string segments.
- */
- class BasePath {
- constructor(segments, offset, length) {
- if (offset === undefined) {
- offset = 0;
- }
- else if (offset > segments.length) {
- fail();
- }
- if (length === undefined) {
- length = segments.length - offset;
- }
- else if (length > segments.length - offset) {
- fail();
- }
- this.segments = segments;
- this.offset = offset;
- this.len = length;
- }
- get length() {
- return this.len;
- }
- isEqual(other) {
- return BasePath.comparator(this, other) === 0;
- }
- child(nameOrPath) {
- const segments = this.segments.slice(this.offset, this.limit());
- if (nameOrPath instanceof BasePath) {
- nameOrPath.forEach(segment => {
- segments.push(segment);
- });
- }
- else {
- segments.push(nameOrPath);
- }
- return this.construct(segments);
- }
- /** The index of one past the last segment of the path. */
- limit() {
- return this.offset + this.length;
- }
- popFirst(size) {
- size = size === undefined ? 1 : size;
- return this.construct(this.segments, this.offset + size, this.length - size);
- }
- popLast() {
- return this.construct(this.segments, this.offset, this.length - 1);
- }
- firstSegment() {
- return this.segments[this.offset];
- }
- lastSegment() {
- return this.get(this.length - 1);
- }
- get(index) {
- return this.segments[this.offset + index];
- }
- isEmpty() {
- return this.length === 0;
- }
- isPrefixOf(other) {
- if (other.length < this.length) {
- return false;
- }
- for (let i = 0; i < this.length; i++) {
- if (this.get(i) !== other.get(i)) {
- return false;
- }
- }
- return true;
- }
- isImmediateParentOf(potentialChild) {
- if (this.length + 1 !== potentialChild.length) {
- return false;
- }
- for (let i = 0; i < this.length; i++) {
- if (this.get(i) !== potentialChild.get(i)) {
- return false;
- }
- }
- return true;
- }
- forEach(fn) {
- for (let i = this.offset, end = this.limit(); i < end; i++) {
- fn(this.segments[i]);
- }
- }
- toArray() {
- return this.segments.slice(this.offset, this.limit());
- }
- static comparator(p1, p2) {
- const len = Math.min(p1.length, p2.length);
- for (let i = 0; i < len; i++) {
- const left = p1.get(i);
- const right = p2.get(i);
- if (left < right) {
- return -1;
- }
- if (left > right) {
- return 1;
- }
- }
- if (p1.length < p2.length) {
- return -1;
- }
- if (p1.length > p2.length) {
- return 1;
- }
- return 0;
- }
- }
- /**
- * A slash-separated path for navigating resources (documents and collections)
- * within Firestore.
- *
- * @internal
- */
- class ResourcePath extends BasePath {
- construct(segments, offset, length) {
- return new ResourcePath(segments, offset, length);
- }
- canonicalString() {
- // NOTE: The client is ignorant of any path segments containing escape
- // sequences (e.g. __id123__) and just passes them through raw (they exist
- // for legacy reasons and should not be used frequently).
- return this.toArray().join('/');
- }
- toString() {
- return this.canonicalString();
- }
- /**
- * Creates a resource path from the given slash-delimited string. If multiple
- * arguments are provided, all components are combined. Leading and trailing
- * slashes from all components are ignored.
- */
- static fromString(...pathComponents) {
- // NOTE: The client is ignorant of any path segments containing escape
- // sequences (e.g. __id123__) and just passes them through raw (they exist
- // for legacy reasons and should not be used frequently).
- const segments = [];
- for (const path of pathComponents) {
- if (path.indexOf('//') >= 0) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid segment (${path}). Paths must not contain // in them.`);
- }
- // Strip leading and traling slashed.
- segments.push(...path.split('/').filter(segment => segment.length > 0));
- }
- return new ResourcePath(segments);
- }
- static emptyPath() {
- return new ResourcePath([]);
- }
- }
- const identifierRegExp = /^[_a-zA-Z][_a-zA-Z0-9]*$/;
- /**
- * A dot-separated path for navigating sub-objects within a document.
- * @internal
- */
- class FieldPath$1 extends BasePath {
- construct(segments, offset, length) {
- return new FieldPath$1(segments, offset, length);
- }
- /**
- * Returns true if the string could be used as a segment in a field path
- * without escaping.
- */
- static isValidIdentifier(segment) {
- return identifierRegExp.test(segment);
- }
- canonicalString() {
- return this.toArray()
- .map(str => {
- str = str.replace(/\\/g, '\\\\').replace(/`/g, '\\`');
- if (!FieldPath$1.isValidIdentifier(str)) {
- str = '`' + str + '`';
- }
- return str;
- })
- .join('.');
- }
- toString() {
- return this.canonicalString();
- }
- /**
- * Returns true if this field references the key of a document.
- */
- isKeyField() {
- return this.length === 1 && this.get(0) === DOCUMENT_KEY_NAME;
- }
- /**
- * The field designating the key of a document.
- */
- static keyField() {
- return new FieldPath$1([DOCUMENT_KEY_NAME]);
- }
- /**
- * Parses a field string from the given server-formatted string.
- *
- * - Splitting the empty string is not allowed (for now at least).
- * - Empty segments within the string (e.g. if there are two consecutive
- * separators) are not allowed.
- *
- * TODO(b/37244157): we should make this more strict. Right now, it allows
- * non-identifier path components, even if they aren't escaped.
- */
- static fromServerFormat(path) {
- const segments = [];
- let current = '';
- let i = 0;
- const addCurrentSegment = () => {
- if (current.length === 0) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid field path (${path}). Paths must not be empty, begin ` +
- `with '.', end with '.', or contain '..'`);
- }
- segments.push(current);
- current = '';
- };
- let inBackticks = false;
- while (i < path.length) {
- const c = path[i];
- if (c === '\\') {
- if (i + 1 === path.length) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Path has trailing escape character: ' + path);
- }
- const next = path[i + 1];
- if (!(next === '\\' || next === '.' || next === '`')) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Path has invalid escape sequence: ' + path);
- }
- current += next;
- i += 2;
- }
- else if (c === '`') {
- inBackticks = !inBackticks;
- i++;
- }
- else if (c === '.' && !inBackticks) {
- addCurrentSegment();
- i++;
- }
- else {
- current += c;
- i++;
- }
- }
- addCurrentSegment();
- if (inBackticks) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Unterminated ` in path: ' + path);
- }
- return new FieldPath$1(segments);
- }
- static emptyPath() {
- return new FieldPath$1([]);
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * @internal
- */
- class DocumentKey {
- constructor(path) {
- this.path = path;
- }
- static fromPath(path) {
- return new DocumentKey(ResourcePath.fromString(path));
- }
- static fromName(name) {
- return new DocumentKey(ResourcePath.fromString(name).popFirst(5));
- }
- static empty() {
- return new DocumentKey(ResourcePath.emptyPath());
- }
- get collectionGroup() {
- return this.path.popLast().lastSegment();
- }
- /** Returns true if the document is in the specified collectionId. */
- hasCollectionId(collectionId) {
- return (this.path.length >= 2 &&
- this.path.get(this.path.length - 2) === collectionId);
- }
- /** Returns the collection group (i.e. the name of the parent collection) for this key. */
- getCollectionGroup() {
- return this.path.get(this.path.length - 2);
- }
- /** Returns the fully qualified path to the parent collection. */
- getCollectionPath() {
- return this.path.popLast();
- }
- isEqual(other) {
- return (other !== null && ResourcePath.comparator(this.path, other.path) === 0);
- }
- toString() {
- return this.path.toString();
- }
- static comparator(k1, k2) {
- return ResourcePath.comparator(k1.path, k2.path);
- }
- static isDocumentKey(path) {
- return path.length % 2 === 0;
- }
- /**
- * Creates and returns a new document key with the given segments.
- *
- * @param segments - The segments of the path to the document
- * @returns A new instance of DocumentKey
- */
- static fromSegments(segments) {
- return new DocumentKey(new ResourcePath(segments.slice()));
- }
- }
- /**
- * @license
- * Copyright 2021 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * The initial mutation batch id for each index. Gets updated during index
- * backfill.
- */
- const INITIAL_LARGEST_BATCH_ID = -1;
- /**
- * The initial sequence number for each index. Gets updated during index
- * backfill.
- */
- const INITIAL_SEQUENCE_NUMBER = 0;
- /**
- * An index definition for field indexes in Firestore.
- *
- * Every index is associated with a collection. The definition contains a list
- * of fields and their index kind (which can be `ASCENDING`, `DESCENDING` or
- * `CONTAINS` for ArrayContains/ArrayContainsAny queries).
- *
- * Unlike the backend, the SDK does not differentiate between collection or
- * collection group-scoped indices. Every index can be used for both single
- * collection and collection group queries.
- */
- class FieldIndex {
- constructor(
- /**
- * The index ID. Returns -1 if the index ID is not available (e.g. the index
- * has not yet been persisted).
- */
- indexId,
- /** The collection ID this index applies to. */
- collectionGroup,
- /** The field segments for this index. */
- fields,
- /** Shows how up-to-date the index is for the current user. */
- indexState) {
- this.indexId = indexId;
- this.collectionGroup = collectionGroup;
- this.fields = fields;
- this.indexState = indexState;
- }
- }
- /** An ID for an index that has not yet been added to persistence. */
- FieldIndex.UNKNOWN_ID = -1;
- /** Returns the ArrayContains/ArrayContainsAny segment for this index. */
- function fieldIndexGetArraySegment(fieldIndex) {
- return fieldIndex.fields.find(s => s.kind === 2 /* IndexKind.CONTAINS */);
- }
- /** Returns all directional (ascending/descending) segments for this index. */
- function fieldIndexGetDirectionalSegments(fieldIndex) {
- return fieldIndex.fields.filter(s => s.kind !== 2 /* IndexKind.CONTAINS */);
- }
- /**
- * Returns the order of the document key component for the given index.
- *
- * PORTING NOTE: This is only used in the Web IndexedDb implementation.
- */
- function fieldIndexGetKeyOrder(fieldIndex) {
- const directionalSegments = fieldIndexGetDirectionalSegments(fieldIndex);
- return directionalSegments.length === 0
- ? 0 /* IndexKind.ASCENDING */
- : directionalSegments[directionalSegments.length - 1].kind;
- }
- /**
- * Compares indexes by collection group and segments. Ignores update time and
- * index ID.
- */
- function fieldIndexSemanticComparator(left, right) {
- let cmp = primitiveComparator(left.collectionGroup, right.collectionGroup);
- if (cmp !== 0) {
- return cmp;
- }
- for (let i = 0; i < Math.min(left.fields.length, right.fields.length); ++i) {
- cmp = indexSegmentComparator(left.fields[i], right.fields[i]);
- if (cmp !== 0) {
- return cmp;
- }
- }
- return primitiveComparator(left.fields.length, right.fields.length);
- }
- /** Returns a debug representation of the field index */
- function fieldIndexToString(fieldIndex) {
- return `id=${fieldIndex.indexId}|cg=${fieldIndex.collectionGroup}|f=${fieldIndex.fields.map(f => `${f.fieldPath}:${f.kind}`).join(',')}`;
- }
- /** An index component consisting of field path and index type. */
- class IndexSegment {
- constructor(
- /** The field path of the component. */
- fieldPath,
- /** The fields sorting order. */
- kind) {
- this.fieldPath = fieldPath;
- this.kind = kind;
- }
- }
- function indexSegmentComparator(left, right) {
- const cmp = FieldPath$1.comparator(left.fieldPath, right.fieldPath);
- if (cmp !== 0) {
- return cmp;
- }
- return primitiveComparator(left.kind, right.kind);
- }
- /**
- * Stores the "high water mark" that indicates how updated the Index is for the
- * current user.
- */
- class IndexState {
- constructor(
- /**
- * Indicates when the index was last updated (relative to other indexes).
- */
- sequenceNumber,
- /** The the latest indexed read time, document and batch id. */
- offset) {
- this.sequenceNumber = sequenceNumber;
- this.offset = offset;
- }
- /** The state of an index that has not yet been backfilled. */
- static empty() {
- return new IndexState(INITIAL_SEQUENCE_NUMBER, IndexOffset.min());
- }
- }
- /**
- * Creates an offset that matches all documents with a read time higher than
- * `readTime`.
- */
- function newIndexOffsetSuccessorFromReadTime(readTime, largestBatchId) {
- // We want to create an offset that matches all documents with a read time
- // greater than the provided read time. To do so, we technically need to
- // create an offset for `(readTime, MAX_DOCUMENT_KEY)`. While we could use
- // Unicode codepoints to generate MAX_DOCUMENT_KEY, it is much easier to use
- // `(readTime + 1, DocumentKey.empty())` since `> DocumentKey.empty()` matches
- // all valid document IDs.
- const successorSeconds = readTime.toTimestamp().seconds;
- const successorNanos = readTime.toTimestamp().nanoseconds + 1;
- const successor = SnapshotVersion.fromTimestamp(successorNanos === 1e9
- ? new Timestamp(successorSeconds + 1, 0)
- : new Timestamp(successorSeconds, successorNanos));
- return new IndexOffset(successor, DocumentKey.empty(), largestBatchId);
- }
- /** Creates a new offset based on the provided document. */
- function newIndexOffsetFromDocument(document) {
- return new IndexOffset(document.readTime, document.key, INITIAL_LARGEST_BATCH_ID);
- }
- /**
- * Stores the latest read time, document and batch ID that were processed for an
- * index.
- */
- class IndexOffset {
- constructor(
- /**
- * The latest read time version that has been indexed by Firestore for this
- * field index.
- */
- readTime,
- /**
- * The key of the last document that was indexed for this query. Use
- * `DocumentKey.empty()` if no document has been indexed.
- */
- documentKey,
- /*
- * The largest mutation batch id that's been processed by Firestore.
- */
- largestBatchId) {
- this.readTime = readTime;
- this.documentKey = documentKey;
- this.largestBatchId = largestBatchId;
- }
- /** Returns an offset that sorts before all regular offsets. */
- static min() {
- return new IndexOffset(SnapshotVersion.min(), DocumentKey.empty(), INITIAL_LARGEST_BATCH_ID);
- }
- /** Returns an offset that sorts after all regular offsets. */
- static max() {
- return new IndexOffset(SnapshotVersion.max(), DocumentKey.empty(), INITIAL_LARGEST_BATCH_ID);
- }
- }
- function indexOffsetComparator(left, right) {
- let cmp = left.readTime.compareTo(right.readTime);
- if (cmp !== 0) {
- return cmp;
- }
- cmp = DocumentKey.comparator(left.documentKey, right.documentKey);
- if (cmp !== 0) {
- return cmp;
- }
- return primitiveComparator(left.largestBatchId, right.largestBatchId);
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const PRIMARY_LEASE_LOST_ERROR_MSG = 'The current tab is not in the required state to perform this operation. ' +
- 'It might be necessary to refresh the browser tab.';
- /**
- * A base class representing a persistence transaction, encapsulating both the
- * transaction's sequence numbers as well as a list of onCommitted listeners.
- *
- * When you call Persistence.runTransaction(), it will create a transaction and
- * pass it to your callback. You then pass it to any method that operates
- * on persistence.
- */
- class PersistenceTransaction {
- constructor() {
- this.onCommittedListeners = [];
- }
- addOnCommittedListener(listener) {
- this.onCommittedListeners.push(listener);
- }
- raiseOnCommittedEvent() {
- this.onCommittedListeners.forEach(listener => listener());
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Verifies the error thrown by a LocalStore operation. If a LocalStore
- * operation fails because the primary lease has been taken by another client,
- * we ignore the error (the persistence layer will immediately call
- * `applyPrimaryLease` to propagate the primary state change). All other errors
- * are re-thrown.
- *
- * @param err - An error returned by a LocalStore operation.
- * @returns A Promise that resolves after we recovered, or the original error.
- */
- async function ignoreIfPrimaryLeaseLoss(err) {
- if (err.code === Code.FAILED_PRECONDITION &&
- err.message === PRIMARY_LEASE_LOST_ERROR_MSG) {
- logDebug('LocalStore', 'Unexpectedly lost primary lease');
- }
- else {
- throw err;
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * PersistencePromise is essentially a re-implementation of Promise except
- * it has a .next() method instead of .then() and .next() and .catch() callbacks
- * are executed synchronously when a PersistencePromise resolves rather than
- * asynchronously (Promise implementations use setImmediate() or similar).
- *
- * This is necessary to interoperate with IndexedDB which will automatically
- * commit transactions if control is returned to the event loop without
- * synchronously initiating another operation on the transaction.
- *
- * NOTE: .then() and .catch() only allow a single consumer, unlike normal
- * Promises.
- */
- class PersistencePromise {
- constructor(callback) {
- // NOTE: next/catchCallback will always point to our own wrapper functions,
- // not the user's raw next() or catch() callbacks.
- this.nextCallback = null;
- this.catchCallback = null;
- // When the operation resolves, we'll set result or error and mark isDone.
- this.result = undefined;
- this.error = undefined;
- this.isDone = false;
- // Set to true when .then() or .catch() are called and prevents additional
- // chaining.
- this.callbackAttached = false;
- callback(value => {
- this.isDone = true;
- this.result = value;
- if (this.nextCallback) {
- // value should be defined unless T is Void, but we can't express
- // that in the type system.
- this.nextCallback(value);
- }
- }, error => {
- this.isDone = true;
- this.error = error;
- if (this.catchCallback) {
- this.catchCallback(error);
- }
- });
- }
- catch(fn) {
- return this.next(undefined, fn);
- }
- next(nextFn, catchFn) {
- if (this.callbackAttached) {
- fail();
- }
- this.callbackAttached = true;
- if (this.isDone) {
- if (!this.error) {
- return this.wrapSuccess(nextFn, this.result);
- }
- else {
- return this.wrapFailure(catchFn, this.error);
- }
- }
- else {
- return new PersistencePromise((resolve, reject) => {
- this.nextCallback = (value) => {
- this.wrapSuccess(nextFn, value).next(resolve, reject);
- };
- this.catchCallback = (error) => {
- this.wrapFailure(catchFn, error).next(resolve, reject);
- };
- });
- }
- }
- toPromise() {
- return new Promise((resolve, reject) => {
- this.next(resolve, reject);
- });
- }
- wrapUserFunction(fn) {
- try {
- const result = fn();
- if (result instanceof PersistencePromise) {
- return result;
- }
- else {
- return PersistencePromise.resolve(result);
- }
- }
- catch (e) {
- return PersistencePromise.reject(e);
- }
- }
- wrapSuccess(nextFn, value) {
- if (nextFn) {
- return this.wrapUserFunction(() => nextFn(value));
- }
- else {
- // If there's no nextFn, then R must be the same as T
- return PersistencePromise.resolve(value);
- }
- }
- wrapFailure(catchFn, error) {
- if (catchFn) {
- return this.wrapUserFunction(() => catchFn(error));
- }
- else {
- return PersistencePromise.reject(error);
- }
- }
- static resolve(result) {
- return new PersistencePromise((resolve, reject) => {
- resolve(result);
- });
- }
- static reject(error) {
- return new PersistencePromise((resolve, reject) => {
- reject(error);
- });
- }
- static waitFor(
- // Accept all Promise types in waitFor().
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- all) {
- return new PersistencePromise((resolve, reject) => {
- let expectedCount = 0;
- let resolvedCount = 0;
- let done = false;
- all.forEach(element => {
- ++expectedCount;
- element.next(() => {
- ++resolvedCount;
- if (done && resolvedCount === expectedCount) {
- resolve();
- }
- }, err => reject(err));
- });
- done = true;
- if (resolvedCount === expectedCount) {
- resolve();
- }
- });
- }
- /**
- * Given an array of predicate functions that asynchronously evaluate to a
- * boolean, implements a short-circuiting `or` between the results. Predicates
- * will be evaluated until one of them returns `true`, then stop. The final
- * result will be whether any of them returned `true`.
- */
- static or(predicates) {
- let p = PersistencePromise.resolve(false);
- for (const predicate of predicates) {
- p = p.next(isTrue => {
- if (isTrue) {
- return PersistencePromise.resolve(isTrue);
- }
- else {
- return predicate();
- }
- });
- }
- return p;
- }
- static forEach(collection, f) {
- const promises = [];
- collection.forEach((r, s) => {
- promises.push(f.call(this, r, s));
- });
- return this.waitFor(promises);
- }
- /**
- * Concurrently map all array elements through asynchronous function.
- */
- static mapArray(array, f) {
- return new PersistencePromise((resolve, reject) => {
- const expectedCount = array.length;
- const results = new Array(expectedCount);
- let resolvedCount = 0;
- for (let i = 0; i < expectedCount; i++) {
- const current = i;
- f(array[current]).next(result => {
- results[current] = result;
- ++resolvedCount;
- if (resolvedCount === expectedCount) {
- resolve(results);
- }
- }, err => reject(err));
- }
- });
- }
- /**
- * An alternative to recursive PersistencePromise calls, that avoids
- * potential memory problems from unbounded chains of promises.
- *
- * The `action` will be called repeatedly while `condition` is true.
- */
- static doWhile(condition, action) {
- return new PersistencePromise((resolve, reject) => {
- const process = () => {
- if (condition() === true) {
- action().next(() => {
- process();
- }, reject);
- }
- else {
- resolve();
- }
- };
- process();
- });
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- // References to `window` are guarded by SimpleDb.isAvailable()
- /* eslint-disable no-restricted-globals */
- const LOG_TAG$i = 'SimpleDb';
- /**
- * The maximum number of retry attempts for an IndexedDb transaction that fails
- * with a DOMException.
- */
- const TRANSACTION_RETRY_COUNT = 3;
- /**
- * Wraps an IDBTransaction and exposes a store() method to get a handle to a
- * specific object store.
- */
- class SimpleDbTransaction {
- constructor(action, transaction) {
- this.action = action;
- this.transaction = transaction;
- this.aborted = false;
- /**
- * A `Promise` that resolves with the result of the IndexedDb transaction.
- */
- this.completionDeferred = new Deferred();
- this.transaction.oncomplete = () => {
- this.completionDeferred.resolve();
- };
- this.transaction.onabort = () => {
- if (transaction.error) {
- this.completionDeferred.reject(new IndexedDbTransactionError(action, transaction.error));
- }
- else {
- this.completionDeferred.resolve();
- }
- };
- this.transaction.onerror = (event) => {
- const error = checkForAndReportiOSError(event.target.error);
- this.completionDeferred.reject(new IndexedDbTransactionError(action, error));
- };
- }
- static open(db, action, mode, objectStoreNames) {
- try {
- return new SimpleDbTransaction(action, db.transaction(objectStoreNames, mode));
- }
- catch (e) {
- throw new IndexedDbTransactionError(action, e);
- }
- }
- get completionPromise() {
- return this.completionDeferred.promise;
- }
- abort(error) {
- if (error) {
- this.completionDeferred.reject(error);
- }
- if (!this.aborted) {
- logDebug(LOG_TAG$i, 'Aborting transaction:', error ? error.message : 'Client-initiated abort');
- this.aborted = true;
- this.transaction.abort();
- }
- }
- maybeCommit() {
- // If the browser supports V3 IndexedDB, we invoke commit() explicitly to
- // speed up index DB processing if the event loop remains blocks.
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const maybeV3IndexedDb = this.transaction;
- if (!this.aborted && typeof maybeV3IndexedDb.commit === 'function') {
- maybeV3IndexedDb.commit();
- }
- }
- /**
- * Returns a SimpleDbStore<KeyType, ValueType> for the specified store. All
- * operations performed on the SimpleDbStore happen within the context of this
- * transaction and it cannot be used anymore once the transaction is
- * completed.
- *
- * Note that we can't actually enforce that the KeyType and ValueType are
- * correct, but they allow type safety through the rest of the consuming code.
- */
- store(storeName) {
- const store = this.transaction.objectStore(storeName);
- return new SimpleDbStore(store);
- }
- }
- /**
- * Provides a wrapper around IndexedDb with a simplified interface that uses
- * Promise-like return values to chain operations. Real promises cannot be used
- * since .then() continuations are executed asynchronously (e.g. via
- * .setImmediate), which would cause IndexedDB to end the transaction.
- * See PersistencePromise for more details.
- */
- class SimpleDb {
- /*
- * Creates a new SimpleDb wrapper for IndexedDb database `name`.
- *
- * Note that `version` must not be a downgrade. IndexedDB does not support
- * downgrading the schema version. We currently do not support any way to do
- * versioning outside of IndexedDB's versioning mechanism, as only
- * version-upgrade transactions are allowed to do things like create
- * objectstores.
- */
- constructor(name, version, schemaConverter) {
- this.name = name;
- this.version = version;
- this.schemaConverter = schemaConverter;
- const iOSVersion = SimpleDb.getIOSVersion(util.getUA());
- // NOTE: According to https://bugs.webkit.org/show_bug.cgi?id=197050, the
- // bug we're checking for should exist in iOS >= 12.2 and < 13, but for
- // whatever reason it's much harder to hit after 12.2 so we only proactively
- // log on 12.2.
- if (iOSVersion === 12.2) {
- logError('Firestore persistence suffers from a bug in iOS 12.2 ' +
- 'Safari that may cause your app to stop working. See ' +
- 'https://stackoverflow.com/q/56496296/110915 for details ' +
- 'and a potential workaround.');
- }
- }
- /** Deletes the specified database. */
- static delete(name) {
- logDebug(LOG_TAG$i, 'Removing database:', name);
- return wrapRequest(window.indexedDB.deleteDatabase(name)).toPromise();
- }
- /** Returns true if IndexedDB is available in the current environment. */
- static isAvailable() {
- if (!util.isIndexedDBAvailable()) {
- return false;
- }
- if (SimpleDb.isMockPersistence()) {
- return true;
- }
- // We extensively use indexed array values and compound keys,
- // which IE and Edge do not support. However, they still have indexedDB
- // defined on the window, so we need to check for them here and make sure
- // to return that persistence is not enabled for those browsers.
- // For tracking support of this feature, see here:
- // https://developer.microsoft.com/en-us/microsoft-edge/platform/status/indexeddbarraysandmultientrysupport/
- // Check the UA string to find out the browser.
- const ua = util.getUA();
- // IE 10
- // ua = 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)';
- // IE 11
- // ua = 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko';
- // Edge
- // ua = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML,
- // like Gecko) Chrome/39.0.2171.71 Safari/537.36 Edge/12.0';
- // iOS Safari: Disable for users running iOS version < 10.
- const iOSVersion = SimpleDb.getIOSVersion(ua);
- const isUnsupportedIOS = 0 < iOSVersion && iOSVersion < 10;
- // Android browser: Disable for userse running version < 4.5.
- const androidVersion = SimpleDb.getAndroidVersion(ua);
- const isUnsupportedAndroid = 0 < androidVersion && androidVersion < 4.5;
- if (ua.indexOf('MSIE ') > 0 ||
- ua.indexOf('Trident/') > 0 ||
- ua.indexOf('Edge/') > 0 ||
- isUnsupportedIOS ||
- isUnsupportedAndroid) {
- return false;
- }
- else {
- return true;
- }
- }
- /**
- * Returns true if the backing IndexedDB store is the Node IndexedDBShim
- * (see https://github.com/axemclion/IndexedDBShim).
- */
- static isMockPersistence() {
- var _a;
- return (typeof process !== 'undefined' &&
- ((_a = process.env) === null || _a === void 0 ? void 0 : _a.USE_MOCK_PERSISTENCE) === 'YES');
- }
- /** Helper to get a typed SimpleDbStore from a transaction. */
- static getStore(txn, store) {
- return txn.store(store);
- }
- // visible for testing
- /** Parse User Agent to determine iOS version. Returns -1 if not found. */
- static getIOSVersion(ua) {
- const iOSVersionRegex = ua.match(/i(?:phone|pad|pod) os ([\d_]+)/i);
- const version = iOSVersionRegex
- ? iOSVersionRegex[1].split('_').slice(0, 2).join('.')
- : '-1';
- return Number(version);
- }
- // visible for testing
- /** Parse User Agent to determine Android version. Returns -1 if not found. */
- static getAndroidVersion(ua) {
- const androidVersionRegex = ua.match(/Android ([\d.]+)/i);
- const version = androidVersionRegex
- ? androidVersionRegex[1].split('.').slice(0, 2).join('.')
- : '-1';
- return Number(version);
- }
- /**
- * Opens the specified database, creating or upgrading it if necessary.
- */
- async ensureDb(action) {
- if (!this.db) {
- logDebug(LOG_TAG$i, 'Opening database:', this.name);
- this.db = await new Promise((resolve, reject) => {
- // TODO(mikelehen): Investigate browser compatibility.
- // https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
- // suggests IE9 and older WebKit browsers handle upgrade
- // differently. They expect setVersion, as described here:
- // https://developer.mozilla.org/en-US/docs/Web/API/IDBVersionChangeRequest/setVersion
- const request = indexedDB.open(this.name, this.version);
- request.onsuccess = (event) => {
- const db = event.target.result;
- resolve(db);
- };
- request.onblocked = () => {
- reject(new IndexedDbTransactionError(action, 'Cannot upgrade IndexedDB schema while another tab is open. ' +
- 'Close all tabs that access Firestore and reload this page to proceed.'));
- };
- request.onerror = (event) => {
- const error = event.target.error;
- if (error.name === 'VersionError') {
- reject(new FirestoreError(Code.FAILED_PRECONDITION, 'A newer version of the Firestore SDK was previously used and so the persisted ' +
- 'data is not compatible with the version of the SDK you are now using. The SDK ' +
- 'will operate with persistence disabled. If you need persistence, please ' +
- 're-upgrade to a newer version of the SDK or else clear the persisted IndexedDB ' +
- 'data for your app to start fresh.'));
- }
- else if (error.name === 'InvalidStateError') {
- reject(new FirestoreError(Code.FAILED_PRECONDITION, 'Unable to open an IndexedDB connection. This could be due to running in a ' +
- 'private browsing session on a browser whose private browsing sessions do not ' +
- 'support IndexedDB: ' +
- error));
- }
- else {
- reject(new IndexedDbTransactionError(action, error));
- }
- };
- request.onupgradeneeded = (event) => {
- logDebug(LOG_TAG$i, 'Database "' + this.name + '" requires upgrade from version:', event.oldVersion);
- const db = event.target.result;
- this.schemaConverter
- .createOrUpgrade(db, request.transaction, event.oldVersion, this.version)
- .next(() => {
- logDebug(LOG_TAG$i, 'Database upgrade to version ' + this.version + ' complete');
- });
- };
- });
- }
- if (this.versionchangelistener) {
- this.db.onversionchange = event => this.versionchangelistener(event);
- }
- return this.db;
- }
- setVersionChangeListener(versionChangeListener) {
- this.versionchangelistener = versionChangeListener;
- if (this.db) {
- this.db.onversionchange = (event) => {
- return versionChangeListener(event);
- };
- }
- }
- async runTransaction(action, mode, objectStores, transactionFn) {
- const readonly = mode === 'readonly';
- let attemptNumber = 0;
- while (true) {
- ++attemptNumber;
- try {
- this.db = await this.ensureDb(action);
- const transaction = SimpleDbTransaction.open(this.db, action, readonly ? 'readonly' : 'readwrite', objectStores);
- const transactionFnResult = transactionFn(transaction)
- .next(result => {
- transaction.maybeCommit();
- return result;
- })
- .catch(error => {
- // Abort the transaction if there was an error.
- transaction.abort(error);
- // We cannot actually recover, and calling `abort()` will cause the transaction's
- // completion promise to be rejected. This in turn means that we won't use
- // `transactionFnResult` below. We return a rejection here so that we don't add the
- // possibility of returning `void` to the type of `transactionFnResult`.
- return PersistencePromise.reject(error);
- })
- .toPromise();
- // As noted above, errors are propagated by aborting the transaction. So
- // we swallow any error here to avoid the browser logging it as unhandled.
- transactionFnResult.catch(() => { });
- // Wait for the transaction to complete (i.e. IndexedDb's onsuccess event to
- // fire), but still return the original transactionFnResult back to the
- // caller.
- await transaction.completionPromise;
- return transactionFnResult;
- }
- catch (e) {
- const error = e;
- // TODO(schmidt-sebastian): We could probably be smarter about this and
- // not retry exceptions that are likely unrecoverable (such as quota
- // exceeded errors).
- // Note: We cannot use an instanceof check for FirestoreException, since the
- // exception is wrapped in a generic error by our async/await handling.
- const retryable = error.name !== 'FirebaseError' &&
- attemptNumber < TRANSACTION_RETRY_COUNT;
- logDebug(LOG_TAG$i, 'Transaction failed with error:', error.message, 'Retrying:', retryable);
- this.close();
- if (!retryable) {
- return Promise.reject(error);
- }
- }
- }
- }
- close() {
- if (this.db) {
- this.db.close();
- }
- this.db = undefined;
- }
- }
- /**
- * A controller for iterating over a key range or index. It allows an iterate
- * callback to delete the currently-referenced object, or jump to a new key
- * within the key range or index.
- */
- class IterationController {
- constructor(dbCursor) {
- this.dbCursor = dbCursor;
- this.shouldStop = false;
- this.nextKey = null;
- }
- get isDone() {
- return this.shouldStop;
- }
- get skipToKey() {
- return this.nextKey;
- }
- set cursor(value) {
- this.dbCursor = value;
- }
- /**
- * This function can be called to stop iteration at any point.
- */
- done() {
- this.shouldStop = true;
- }
- /**
- * This function can be called to skip to that next key, which could be
- * an index or a primary key.
- */
- skip(key) {
- this.nextKey = key;
- }
- /**
- * Delete the current cursor value from the object store.
- *
- * NOTE: You CANNOT do this with a keysOnly query.
- */
- delete() {
- return wrapRequest(this.dbCursor.delete());
- }
- }
- /** An error that wraps exceptions that thrown during IndexedDB execution. */
- class IndexedDbTransactionError extends FirestoreError {
- constructor(actionName, cause) {
- super(Code.UNAVAILABLE, `IndexedDB transaction '${actionName}' failed: ${cause}`);
- this.name = 'IndexedDbTransactionError';
- }
- }
- /** Verifies whether `e` is an IndexedDbTransactionError. */
- function isIndexedDbTransactionError(e) {
- // Use name equality, as instanceof checks on errors don't work with errors
- // that wrap other errors.
- return e.name === 'IndexedDbTransactionError';
- }
- /**
- * A wrapper around an IDBObjectStore providing an API that:
- *
- * 1) Has generic KeyType / ValueType parameters to provide strongly-typed
- * methods for acting against the object store.
- * 2) Deals with IndexedDB's onsuccess / onerror event callbacks, making every
- * method return a PersistencePromise instead.
- * 3) Provides a higher-level API to avoid needing to do excessive wrapping of
- * intermediate IndexedDB types (IDBCursorWithValue, etc.)
- */
- class SimpleDbStore {
- constructor(store) {
- this.store = store;
- }
- put(keyOrValue, value) {
- let request;
- if (value !== undefined) {
- logDebug(LOG_TAG$i, 'PUT', this.store.name, keyOrValue, value);
- request = this.store.put(value, keyOrValue);
- }
- else {
- logDebug(LOG_TAG$i, 'PUT', this.store.name, '<auto-key>', keyOrValue);
- request = this.store.put(keyOrValue);
- }
- return wrapRequest(request);
- }
- /**
- * Adds a new value into an Object Store and returns the new key. Similar to
- * IndexedDb's `add()`, this method will fail on primary key collisions.
- *
- * @param value - The object to write.
- * @returns The key of the value to add.
- */
- add(value) {
- logDebug(LOG_TAG$i, 'ADD', this.store.name, value, value);
- const request = this.store.add(value);
- return wrapRequest(request);
- }
- /**
- * Gets the object with the specified key from the specified store, or null
- * if no object exists with the specified key.
- *
- * @key The key of the object to get.
- * @returns The object with the specified key or null if no object exists.
- */
- get(key) {
- const request = this.store.get(key);
- // We're doing an unsafe cast to ValueType.
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- return wrapRequest(request).next(result => {
- // Normalize nonexistence to null.
- if (result === undefined) {
- result = null;
- }
- logDebug(LOG_TAG$i, 'GET', this.store.name, key, result);
- return result;
- });
- }
- delete(key) {
- logDebug(LOG_TAG$i, 'DELETE', this.store.name, key);
- const request = this.store.delete(key);
- return wrapRequest(request);
- }
- /**
- * If we ever need more of the count variants, we can add overloads. For now,
- * all we need is to count everything in a store.
- *
- * Returns the number of rows in the store.
- */
- count() {
- logDebug(LOG_TAG$i, 'COUNT', this.store.name);
- const request = this.store.count();
- return wrapRequest(request);
- }
- loadAll(indexOrRange, range) {
- const iterateOptions = this.options(indexOrRange, range);
- // Use `getAll()` if the browser supports IndexedDB v3, as it is roughly
- // 20% faster. Unfortunately, getAll() does not support custom indices.
- if (!iterateOptions.index && typeof this.store.getAll === 'function') {
- const request = this.store.getAll(iterateOptions.range);
- return new PersistencePromise((resolve, reject) => {
- request.onerror = (event) => {
- reject(event.target.error);
- };
- request.onsuccess = (event) => {
- resolve(event.target.result);
- };
- });
- }
- else {
- const cursor = this.cursor(iterateOptions);
- const results = [];
- return this.iterateCursor(cursor, (key, value) => {
- results.push(value);
- }).next(() => {
- return results;
- });
- }
- }
- /**
- * Loads the first `count` elements from the provided index range. Loads all
- * elements if no limit is provided.
- */
- loadFirst(range, count) {
- const request = this.store.getAll(range, count === null ? undefined : count);
- return new PersistencePromise((resolve, reject) => {
- request.onerror = (event) => {
- reject(event.target.error);
- };
- request.onsuccess = (event) => {
- resolve(event.target.result);
- };
- });
- }
- deleteAll(indexOrRange, range) {
- logDebug(LOG_TAG$i, 'DELETE ALL', this.store.name);
- const options = this.options(indexOrRange, range);
- options.keysOnly = false;
- const cursor = this.cursor(options);
- return this.iterateCursor(cursor, (key, value, control) => {
- // NOTE: Calling delete() on a cursor is documented as more efficient than
- // calling delete() on an object store with a single key
- // (https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/delete),
- // however, this requires us *not* to use a keysOnly cursor
- // (https://developer.mozilla.org/en-US/docs/Web/API/IDBCursor/delete). We
- // may want to compare the performance of each method.
- return control.delete();
- });
- }
- iterate(optionsOrCallback, callback) {
- let options;
- if (!callback) {
- options = {};
- callback = optionsOrCallback;
- }
- else {
- options = optionsOrCallback;
- }
- const cursor = this.cursor(options);
- return this.iterateCursor(cursor, callback);
- }
- /**
- * Iterates over a store, but waits for the given callback to complete for
- * each entry before iterating the next entry. This allows the callback to do
- * asynchronous work to determine if this iteration should continue.
- *
- * The provided callback should return `true` to continue iteration, and
- * `false` otherwise.
- */
- iterateSerial(callback) {
- const cursorRequest = this.cursor({});
- return new PersistencePromise((resolve, reject) => {
- cursorRequest.onerror = (event) => {
- const error = checkForAndReportiOSError(event.target.error);
- reject(error);
- };
- cursorRequest.onsuccess = (event) => {
- const cursor = event.target.result;
- if (!cursor) {
- resolve();
- return;
- }
- callback(cursor.primaryKey, cursor.value).next(shouldContinue => {
- if (shouldContinue) {
- cursor.continue();
- }
- else {
- resolve();
- }
- });
- };
- });
- }
- iterateCursor(cursorRequest, fn) {
- const results = [];
- return new PersistencePromise((resolve, reject) => {
- cursorRequest.onerror = (event) => {
- reject(event.target.error);
- };
- cursorRequest.onsuccess = (event) => {
- const cursor = event.target.result;
- if (!cursor) {
- resolve();
- return;
- }
- const controller = new IterationController(cursor);
- const userResult = fn(cursor.primaryKey, cursor.value, controller);
- if (userResult instanceof PersistencePromise) {
- const userPromise = userResult.catch(err => {
- controller.done();
- return PersistencePromise.reject(err);
- });
- results.push(userPromise);
- }
- if (controller.isDone) {
- resolve();
- }
- else if (controller.skipToKey === null) {
- cursor.continue();
- }
- else {
- cursor.continue(controller.skipToKey);
- }
- };
- }).next(() => PersistencePromise.waitFor(results));
- }
- options(indexOrRange, range) {
- let indexName = undefined;
- if (indexOrRange !== undefined) {
- if (typeof indexOrRange === 'string') {
- indexName = indexOrRange;
- }
- else {
- range = indexOrRange;
- }
- }
- return { index: indexName, range };
- }
- cursor(options) {
- let direction = 'next';
- if (options.reverse) {
- direction = 'prev';
- }
- if (options.index) {
- const index = this.store.index(options.index);
- if (options.keysOnly) {
- return index.openKeyCursor(options.range, direction);
- }
- else {
- return index.openCursor(options.range, direction);
- }
- }
- else {
- return this.store.openCursor(options.range, direction);
- }
- }
- }
- /**
- * Wraps an IDBRequest in a PersistencePromise, using the onsuccess / onerror
- * handlers to resolve / reject the PersistencePromise as appropriate.
- */
- function wrapRequest(request) {
- return new PersistencePromise((resolve, reject) => {
- request.onsuccess = (event) => {
- const result = event.target.result;
- resolve(result);
- };
- request.onerror = (event) => {
- const error = checkForAndReportiOSError(event.target.error);
- reject(error);
- };
- });
- }
- // Guard so we only report the error once.
- let reportedIOSError = false;
- function checkForAndReportiOSError(error) {
- const iOSVersion = SimpleDb.getIOSVersion(util.getUA());
- if (iOSVersion >= 12.2 && iOSVersion < 13) {
- const IOS_ERROR = 'An internal error was encountered in the Indexed Database server';
- if (error.message.indexOf(IOS_ERROR) >= 0) {
- // Wrap error in a more descriptive one.
- const newError = new FirestoreError('internal', `IOS_INDEXEDDB_BUG1: IndexedDb has thrown '${IOS_ERROR}'. This is likely ` +
- `due to an unavoidable bug in iOS. See https://stackoverflow.com/q/56496296/110915 ` +
- `for details and a potential workaround.`);
- if (!reportedIOSError) {
- reportedIOSError = true;
- // Throw a global exception outside of this promise chain, for the user to
- // potentially catch.
- setTimeout(() => {
- throw newError;
- }, 0);
- }
- return newError;
- }
- }
- return error;
- }
- const LOG_TAG$h = 'IndexBackiller';
- /** How long we wait to try running index backfill after SDK initialization. */
- const INITIAL_BACKFILL_DELAY_MS = 15 * 1000;
- /** Minimum amount of time between backfill checks, after the first one. */
- const REGULAR_BACKFILL_DELAY_MS = 60 * 1000;
- /** The maximum number of documents to process each time backfill() is called. */
- const MAX_DOCUMENTS_TO_PROCESS = 50;
- /** This class is responsible for the scheduling of Index Backfiller. */
- class IndexBackfillerScheduler {
- constructor(asyncQueue, backfiller) {
- this.asyncQueue = asyncQueue;
- this.backfiller = backfiller;
- this.task = null;
- }
- start() {
- this.schedule(INITIAL_BACKFILL_DELAY_MS);
- }
- stop() {
- if (this.task) {
- this.task.cancel();
- this.task = null;
- }
- }
- get started() {
- return this.task !== null;
- }
- schedule(delay) {
- logDebug(LOG_TAG$h, `Scheduled in ${delay}ms`);
- this.task = this.asyncQueue.enqueueAfterDelay("index_backfill" /* TimerId.IndexBackfill */, delay, async () => {
- this.task = null;
- try {
- const documentsProcessed = await this.backfiller.backfill();
- logDebug(LOG_TAG$h, `Documents written: ${documentsProcessed}`);
- }
- catch (e) {
- if (isIndexedDbTransactionError(e)) {
- logDebug(LOG_TAG$h, 'Ignoring IndexedDB error during index backfill: ', e);
- }
- else {
- await ignoreIfPrimaryLeaseLoss(e);
- }
- }
- await this.schedule(REGULAR_BACKFILL_DELAY_MS);
- });
- }
- }
- /** Implements the steps for backfilling indexes. */
- class IndexBackfiller {
- constructor(
- /**
- * LocalStore provides access to IndexManager and LocalDocumentView.
- * These properties will update when the user changes. Consequently,
- * making a local copy of IndexManager and LocalDocumentView will require
- * updates over time. The simpler solution is to rely on LocalStore to have
- * an up-to-date references to IndexManager and LocalDocumentStore.
- */
- localStore, persistence) {
- this.localStore = localStore;
- this.persistence = persistence;
- }
- async backfill(maxDocumentsToProcess = MAX_DOCUMENTS_TO_PROCESS) {
- return this.persistence.runTransaction('Backfill Indexes', 'readwrite-primary', txn => this.writeIndexEntries(txn, maxDocumentsToProcess));
- }
- /** Writes index entries until the cap is reached. Returns the number of documents processed. */
- writeIndexEntries(transation, maxDocumentsToProcess) {
- const processedCollectionGroups = new Set();
- let documentsRemaining = maxDocumentsToProcess;
- let continueLoop = true;
- return PersistencePromise.doWhile(() => continueLoop === true && documentsRemaining > 0, () => {
- return this.localStore.indexManager
- .getNextCollectionGroupToUpdate(transation)
- .next((collectionGroup) => {
- if (collectionGroup === null ||
- processedCollectionGroups.has(collectionGroup)) {
- continueLoop = false;
- }
- else {
- logDebug(LOG_TAG$h, `Processing collection: ${collectionGroup}`);
- return this.writeEntriesForCollectionGroup(transation, collectionGroup, documentsRemaining).next(documentsProcessed => {
- documentsRemaining -= documentsProcessed;
- processedCollectionGroups.add(collectionGroup);
- });
- }
- });
- }).next(() => maxDocumentsToProcess - documentsRemaining);
- }
- /**
- * Writes entries for the provided collection group. Returns the number of documents processed.
- */
- writeEntriesForCollectionGroup(transaction, collectionGroup, documentsRemainingUnderCap) {
- // Use the earliest offset of all field indexes to query the local cache.
- return this.localStore.indexManager
- .getMinOffsetFromCollectionGroup(transaction, collectionGroup)
- .next(existingOffset => this.localStore.localDocuments
- .getNextDocuments(transaction, collectionGroup, existingOffset, documentsRemainingUnderCap)
- .next(nextBatch => {
- const docs = nextBatch.changes;
- return this.localStore.indexManager
- .updateIndexEntries(transaction, docs)
- .next(() => this.getNewOffset(existingOffset, nextBatch))
- .next(newOffset => {
- logDebug(LOG_TAG$h, `Updating offset: ${newOffset}`);
- return this.localStore.indexManager.updateCollectionGroup(transaction, collectionGroup, newOffset);
- })
- .next(() => docs.size);
- }));
- }
- /** Returns the next offset based on the provided documents. */
- getNewOffset(existingOffset, lookupResult) {
- let maxOffset = existingOffset;
- lookupResult.changes.forEach((key, document) => {
- const newOffset = newIndexOffsetFromDocument(document);
- if (indexOffsetComparator(newOffset, maxOffset) > 0) {
- maxOffset = newOffset;
- }
- });
- return new IndexOffset(maxOffset.readTime, maxOffset.documentKey, Math.max(lookupResult.batchId, existingOffset.largestBatchId));
- }
- }
- /**
- * @license
- * Copyright 2018 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * `ListenSequence` is a monotonic sequence. It is initialized with a minimum value to
- * exceed. All subsequent calls to next will return increasing values. If provided with a
- * `SequenceNumberSyncer`, it will additionally bump its next value when told of a new value, as
- * well as write out sequence numbers that it produces via `next()`.
- */
- class ListenSequence {
- constructor(previousValue, sequenceNumberSyncer) {
- this.previousValue = previousValue;
- if (sequenceNumberSyncer) {
- sequenceNumberSyncer.sequenceNumberHandler = sequenceNumber => this.setPreviousValue(sequenceNumber);
- this.writeNewSequenceNumber = sequenceNumber => sequenceNumberSyncer.writeSequenceNumber(sequenceNumber);
- }
- }
- setPreviousValue(externalPreviousValue) {
- this.previousValue = Math.max(externalPreviousValue, this.previousValue);
- return this.previousValue;
- }
- next() {
- const nextValue = ++this.previousValue;
- if (this.writeNewSequenceNumber) {
- this.writeNewSequenceNumber(nextValue);
- }
- return nextValue;
- }
- }
- ListenSequence.INVALID = -1;
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const escapeChar = '\u0001';
- const encodedSeparatorChar = '\u0001';
- const encodedNul = '\u0010';
- const encodedEscape = '\u0011';
- /**
- * Encodes a resource path into a IndexedDb-compatible string form.
- */
- function encodeResourcePath(path) {
- let result = '';
- for (let i = 0; i < path.length; i++) {
- if (result.length > 0) {
- result = encodeSeparator(result);
- }
- result = encodeSegment(path.get(i), result);
- }
- return encodeSeparator(result);
- }
- /** Encodes a single segment of a resource path into the given result */
- function encodeSegment(segment, resultBuf) {
- let result = resultBuf;
- const length = segment.length;
- for (let i = 0; i < length; i++) {
- const c = segment.charAt(i);
- switch (c) {
- case '\0':
- result += escapeChar + encodedNul;
- break;
- case escapeChar:
- result += escapeChar + encodedEscape;
- break;
- default:
- result += c;
- }
- }
- return result;
- }
- /** Encodes a path separator into the given result */
- function encodeSeparator(result) {
- return result + escapeChar + encodedSeparatorChar;
- }
- /**
- * Decodes the given IndexedDb-compatible string form of a resource path into
- * a ResourcePath instance. Note that this method is not suitable for use with
- * decoding resource names from the server; those are One Platform format
- * strings.
- */
- function decodeResourcePath(path) {
- // Event the empty path must encode as a path of at least length 2. A path
- // with exactly 2 must be the empty path.
- const length = path.length;
- hardAssert(length >= 2);
- if (length === 2) {
- hardAssert(path.charAt(0) === escapeChar && path.charAt(1) === encodedSeparatorChar);
- return ResourcePath.emptyPath();
- }
- // Escape characters cannot exist past the second-to-last position in the
- // source value.
- const lastReasonableEscapeIndex = length - 2;
- const segments = [];
- let segmentBuilder = '';
- for (let start = 0; start < length;) {
- // The last two characters of a valid encoded path must be a separator, so
- // there must be an end to this segment.
- const end = path.indexOf(escapeChar, start);
- if (end < 0 || end > lastReasonableEscapeIndex) {
- fail();
- }
- const next = path.charAt(end + 1);
- switch (next) {
- case encodedSeparatorChar:
- const currentPiece = path.substring(start, end);
- let segment;
- if (segmentBuilder.length === 0) {
- // Avoid copying for the common case of a segment that excludes \0
- // and \001
- segment = currentPiece;
- }
- else {
- segmentBuilder += currentPiece;
- segment = segmentBuilder;
- segmentBuilder = '';
- }
- segments.push(segment);
- break;
- case encodedNul:
- segmentBuilder += path.substring(start, end);
- segmentBuilder += '\0';
- break;
- case encodedEscape:
- // The escape character can be used in the output to encode itself.
- segmentBuilder += path.substring(start, end + 1);
- break;
- default:
- fail();
- }
- start = end + 2;
- }
- return new ResourcePath(segments);
- }
- /**
- * @license
- * Copyright 2022 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const DbRemoteDocumentStore$1 = 'remoteDocuments';
- /**
- * @license
- * Copyright 2022 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Name of the IndexedDb object store.
- *
- * Note that the name 'owner' is chosen to ensure backwards compatibility with
- * older clients that only supported single locked access to the persistence
- * layer.
- */
- const DbPrimaryClientStore = 'owner';
- /**
- * The key string used for the single object that exists in the
- * DbPrimaryClient store.
- */
- const DbPrimaryClientKey = 'owner';
- /** Name of the IndexedDb object store. */
- const DbMutationQueueStore = 'mutationQueues';
- /** Keys are automatically assigned via the userId property. */
- const DbMutationQueueKeyPath = 'userId';
- /** Name of the IndexedDb object store. */
- const DbMutationBatchStore = 'mutations';
- /** Keys are automatically assigned via the userId, batchId properties. */
- const DbMutationBatchKeyPath = 'batchId';
- /** The index name for lookup of mutations by user. */
- const DbMutationBatchUserMutationsIndex = 'userMutationsIndex';
- /** The user mutations index is keyed by [userId, batchId] pairs. */
- const DbMutationBatchUserMutationsKeyPath = ['userId', 'batchId'];
- /**
- * Creates a [userId] key for use in the DbDocumentMutations index to iterate
- * over all of a user's document mutations.
- */
- function newDbDocumentMutationPrefixForUser(userId) {
- return [userId];
- }
- /**
- * Creates a [userId, encodedPath] key for use in the DbDocumentMutations
- * index to iterate over all at document mutations for a given path or lower.
- */
- function newDbDocumentMutationPrefixForPath(userId, path) {
- return [userId, encodeResourcePath(path)];
- }
- /**
- * Creates a full index key of [userId, encodedPath, batchId] for inserting
- * and deleting into the DbDocumentMutations index.
- */
- function newDbDocumentMutationKey(userId, path, batchId) {
- return [userId, encodeResourcePath(path), batchId];
- }
- /**
- * Because we store all the useful information for this store in the key,
- * there is no useful information to store as the value. The raw (unencoded)
- * path cannot be stored because IndexedDb doesn't store prototype
- * information.
- */
- const DbDocumentMutationPlaceholder = {};
- const DbDocumentMutationStore = 'documentMutations';
- const DbRemoteDocumentStore = 'remoteDocumentsV14';
- /**
- * The primary key of the remote documents store, which allows for efficient
- * access by collection path and read time.
- */
- const DbRemoteDocumentKeyPath = [
- 'prefixPath',
- 'collectionGroup',
- 'readTime',
- 'documentId'
- ];
- /** An index that provides access to documents by key. */
- const DbRemoteDocumentDocumentKeyIndex = 'documentKeyIndex';
- const DbRemoteDocumentDocumentKeyIndexPath = [
- 'prefixPath',
- 'collectionGroup',
- 'documentId'
- ];
- /**
- * An index that provides access to documents by collection group and read
- * time.
- *
- * This index is used by the index backfiller.
- */
- const DbRemoteDocumentCollectionGroupIndex = 'collectionGroupIndex';
- const DbRemoteDocumentCollectionGroupIndexPath = [
- 'collectionGroup',
- 'readTime',
- 'prefixPath',
- 'documentId'
- ];
- const DbRemoteDocumentGlobalStore = 'remoteDocumentGlobal';
- const DbRemoteDocumentGlobalKey = 'remoteDocumentGlobalKey';
- const DbTargetStore = 'targets';
- /** Keys are automatically assigned via the targetId property. */
- const DbTargetKeyPath = 'targetId';
- /** The name of the queryTargets index. */
- const DbTargetQueryTargetsIndexName = 'queryTargetsIndex';
- /**
- * The index of all canonicalIds to the targets that they match. This is not
- * a unique mapping because canonicalId does not promise a unique name for all
- * possible queries, so we append the targetId to make the mapping unique.
- */
- const DbTargetQueryTargetsKeyPath = ['canonicalId', 'targetId'];
- /** Name of the IndexedDb object store. */
- const DbTargetDocumentStore = 'targetDocuments';
- /** Keys are automatically assigned via the targetId, path properties. */
- const DbTargetDocumentKeyPath = ['targetId', 'path'];
- /** The index name for the reverse index. */
- const DbTargetDocumentDocumentTargetsIndex = 'documentTargetsIndex';
- /** We also need to create the reverse index for these properties. */
- const DbTargetDocumentDocumentTargetsKeyPath = ['path', 'targetId'];
- /**
- * The key string used for the single object that exists in the
- * DbTargetGlobal store.
- */
- const DbTargetGlobalKey = 'targetGlobalKey';
- const DbTargetGlobalStore = 'targetGlobal';
- /** Name of the IndexedDb object store. */
- const DbCollectionParentStore = 'collectionParents';
- /** Keys are automatically assigned via the collectionId, parent properties. */
- const DbCollectionParentKeyPath = ['collectionId', 'parent'];
- /** Name of the IndexedDb object store. */
- const DbClientMetadataStore = 'clientMetadata';
- /** Keys are automatically assigned via the clientId properties. */
- const DbClientMetadataKeyPath = 'clientId';
- /** Name of the IndexedDb object store. */
- const DbBundleStore = 'bundles';
- const DbBundleKeyPath = 'bundleId';
- /** Name of the IndexedDb object store. */
- const DbNamedQueryStore = 'namedQueries';
- const DbNamedQueryKeyPath = 'name';
- /** Name of the IndexedDb object store. */
- const DbIndexConfigurationStore = 'indexConfiguration';
- const DbIndexConfigurationKeyPath = 'indexId';
- /**
- * An index that provides access to the index configurations by collection
- * group.
- *
- * PORTING NOTE: iOS and Android maintain this index in-memory, but this is
- * not possible here as the Web client supports concurrent access to
- * persistence via multi-tab.
- */
- const DbIndexConfigurationCollectionGroupIndex = 'collectionGroupIndex';
- const DbIndexConfigurationCollectionGroupIndexPath = 'collectionGroup';
- /** Name of the IndexedDb object store. */
- const DbIndexStateStore = 'indexState';
- const DbIndexStateKeyPath = ['indexId', 'uid'];
- /**
- * An index that provides access to documents in a collection sorted by last
- * update time. Used by the backfiller.
- *
- * PORTING NOTE: iOS and Android maintain this index in-memory, but this is
- * not possible here as the Web client supports concurrent access to
- * persistence via multi-tab.
- */
- const DbIndexStateSequenceNumberIndex = 'sequenceNumberIndex';
- const DbIndexStateSequenceNumberIndexPath = ['uid', 'sequenceNumber'];
- /** Name of the IndexedDb object store. */
- const DbIndexEntryStore = 'indexEntries';
- const DbIndexEntryKeyPath = [
- 'indexId',
- 'uid',
- 'arrayValue',
- 'directionalValue',
- 'orderedDocumentKey',
- 'documentKey'
- ];
- const DbIndexEntryDocumentKeyIndex = 'documentKeyIndex';
- const DbIndexEntryDocumentKeyIndexPath = [
- 'indexId',
- 'uid',
- 'orderedDocumentKey'
- ];
- /** Name of the IndexedDb object store. */
- const DbDocumentOverlayStore = 'documentOverlays';
- const DbDocumentOverlayKeyPath = [
- 'userId',
- 'collectionPath',
- 'documentId'
- ];
- const DbDocumentOverlayCollectionPathOverlayIndex = 'collectionPathOverlayIndex';
- const DbDocumentOverlayCollectionPathOverlayIndexPath = [
- 'userId',
- 'collectionPath',
- 'largestBatchId'
- ];
- const DbDocumentOverlayCollectionGroupOverlayIndex = 'collectionGroupOverlayIndex';
- const DbDocumentOverlayCollectionGroupOverlayIndexPath = [
- 'userId',
- 'collectionGroup',
- 'largestBatchId'
- ];
- // Visible for testing
- const V1_STORES = [
- DbMutationQueueStore,
- DbMutationBatchStore,
- DbDocumentMutationStore,
- DbRemoteDocumentStore$1,
- DbTargetStore,
- DbPrimaryClientStore,
- DbTargetGlobalStore,
- DbTargetDocumentStore
- ];
- // Visible for testing
- const V3_STORES = V1_STORES;
- // Note: DbRemoteDocumentChanges is no longer used and dropped with v9.
- const V4_STORES = [...V3_STORES, DbClientMetadataStore];
- const V6_STORES = [...V4_STORES, DbRemoteDocumentGlobalStore];
- const V8_STORES = [...V6_STORES, DbCollectionParentStore];
- const V11_STORES = [...V8_STORES, DbBundleStore, DbNamedQueryStore];
- const V12_STORES = [...V11_STORES, DbDocumentOverlayStore];
- const V13_STORES = [
- DbMutationQueueStore,
- DbMutationBatchStore,
- DbDocumentMutationStore,
- DbRemoteDocumentStore,
- DbTargetStore,
- DbPrimaryClientStore,
- DbTargetGlobalStore,
- DbTargetDocumentStore,
- DbClientMetadataStore,
- DbRemoteDocumentGlobalStore,
- DbCollectionParentStore,
- DbBundleStore,
- DbNamedQueryStore,
- DbDocumentOverlayStore
- ];
- const V14_STORES = V13_STORES;
- const V15_STORES = [
- ...V14_STORES,
- DbIndexConfigurationStore,
- DbIndexStateStore,
- DbIndexEntryStore
- ];
- /** Returns the object stores for the provided schema. */
- function getObjectStores(schemaVersion) {
- if (schemaVersion === 15) {
- return V15_STORES;
- }
- else if (schemaVersion === 14) {
- return V14_STORES;
- }
- else if (schemaVersion === 13) {
- return V13_STORES;
- }
- else if (schemaVersion === 12) {
- return V12_STORES;
- }
- else if (schemaVersion === 11) {
- return V11_STORES;
- }
- else {
- fail();
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- class IndexedDbTransaction extends PersistenceTransaction {
- constructor(simpleDbTransaction, currentSequenceNumber) {
- super();
- this.simpleDbTransaction = simpleDbTransaction;
- this.currentSequenceNumber = currentSequenceNumber;
- }
- }
- function getStore(txn, store) {
- const indexedDbTransaction = debugCast(txn);
- return SimpleDb.getStore(indexedDbTransaction.simpleDbTransaction, store);
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- function objectSize(obj) {
- let count = 0;
- for (const key in obj) {
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
- count++;
- }
- }
- return count;
- }
- function forEach(obj, fn) {
- for (const key in obj) {
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
- fn(key, obj[key]);
- }
- }
- }
- function mapToArray(obj, fn) {
- const result = [];
- for (const key in obj) {
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
- result.push(fn(obj[key], key, obj));
- }
- }
- return result;
- }
- function isEmpty(obj) {
- for (const key in obj) {
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
- return false;
- }
- }
- return true;
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- // An immutable sorted map implementation, based on a Left-leaning Red-Black
- // tree.
- class SortedMap {
- constructor(comparator, root) {
- this.comparator = comparator;
- this.root = root ? root : LLRBNode.EMPTY;
- }
- // Returns a copy of the map, with the specified key/value added or replaced.
- insert(key, value) {
- return new SortedMap(this.comparator, this.root
- .insert(key, value, this.comparator)
- .copy(null, null, LLRBNode.BLACK, null, null));
- }
- // Returns a copy of the map, with the specified key removed.
- remove(key) {
- return new SortedMap(this.comparator, this.root
- .remove(key, this.comparator)
- .copy(null, null, LLRBNode.BLACK, null, null));
- }
- // Returns the value of the node with the given key, or null.
- get(key) {
- let node = this.root;
- while (!node.isEmpty()) {
- const cmp = this.comparator(key, node.key);
- if (cmp === 0) {
- return node.value;
- }
- else if (cmp < 0) {
- node = node.left;
- }
- else if (cmp > 0) {
- node = node.right;
- }
- }
- return null;
- }
- // Returns the index of the element in this sorted map, or -1 if it doesn't
- // exist.
- indexOf(key) {
- // Number of nodes that were pruned when descending right
- let prunedNodes = 0;
- let node = this.root;
- while (!node.isEmpty()) {
- const cmp = this.comparator(key, node.key);
- if (cmp === 0) {
- return prunedNodes + node.left.size;
- }
- else if (cmp < 0) {
- node = node.left;
- }
- else {
- // Count all nodes left of the node plus the node itself
- prunedNodes += node.left.size + 1;
- node = node.right;
- }
- }
- // Node not found
- return -1;
- }
- isEmpty() {
- return this.root.isEmpty();
- }
- // Returns the total number of nodes in the map.
- get size() {
- return this.root.size;
- }
- // Returns the minimum key in the map.
- minKey() {
- return this.root.minKey();
- }
- // Returns the maximum key in the map.
- maxKey() {
- return this.root.maxKey();
- }
- // Traverses the map in key order and calls the specified action function
- // for each key/value pair. If action returns true, traversal is aborted.
- // Returns the first truthy value returned by action, or the last falsey
- // value returned by action.
- inorderTraversal(action) {
- return this.root.inorderTraversal(action);
- }
- forEach(fn) {
- this.inorderTraversal((k, v) => {
- fn(k, v);
- return false;
- });
- }
- toString() {
- const descriptions = [];
- this.inorderTraversal((k, v) => {
- descriptions.push(`${k}:${v}`);
- return false;
- });
- return `{${descriptions.join(', ')}}`;
- }
- // Traverses the map in reverse key order and calls the specified action
- // function for each key/value pair. If action returns true, traversal is
- // aborted.
- // Returns the first truthy value returned by action, or the last falsey
- // value returned by action.
- reverseTraversal(action) {
- return this.root.reverseTraversal(action);
- }
- // Returns an iterator over the SortedMap.
- getIterator() {
- return new SortedMapIterator(this.root, null, this.comparator, false);
- }
- getIteratorFrom(key) {
- return new SortedMapIterator(this.root, key, this.comparator, false);
- }
- getReverseIterator() {
- return new SortedMapIterator(this.root, null, this.comparator, true);
- }
- getReverseIteratorFrom(key) {
- return new SortedMapIterator(this.root, key, this.comparator, true);
- }
- } // end SortedMap
- // An iterator over an LLRBNode.
- class SortedMapIterator {
- constructor(node, startKey, comparator, isReverse) {
- this.isReverse = isReverse;
- this.nodeStack = [];
- let cmp = 1;
- while (!node.isEmpty()) {
- cmp = startKey ? comparator(node.key, startKey) : 1;
- // flip the comparison if we're going in reverse
- if (startKey && isReverse) {
- cmp *= -1;
- }
- if (cmp < 0) {
- // This node is less than our start key. ignore it
- if (this.isReverse) {
- node = node.left;
- }
- else {
- node = node.right;
- }
- }
- else if (cmp === 0) {
- // This node is exactly equal to our start key. Push it on the stack,
- // but stop iterating;
- this.nodeStack.push(node);
- break;
- }
- else {
- // This node is greater than our start key, add it to the stack and move
- // to the next one
- this.nodeStack.push(node);
- if (this.isReverse) {
- node = node.right;
- }
- else {
- node = node.left;
- }
- }
- }
- }
- getNext() {
- let node = this.nodeStack.pop();
- const result = { key: node.key, value: node.value };
- if (this.isReverse) {
- node = node.left;
- while (!node.isEmpty()) {
- this.nodeStack.push(node);
- node = node.right;
- }
- }
- else {
- node = node.right;
- while (!node.isEmpty()) {
- this.nodeStack.push(node);
- node = node.left;
- }
- }
- return result;
- }
- hasNext() {
- return this.nodeStack.length > 0;
- }
- peek() {
- if (this.nodeStack.length === 0) {
- return null;
- }
- const node = this.nodeStack[this.nodeStack.length - 1];
- return { key: node.key, value: node.value };
- }
- } // end SortedMapIterator
- // Represents a node in a Left-leaning Red-Black tree.
- class LLRBNode {
- constructor(key, value, color, left, right) {
- this.key = key;
- this.value = value;
- this.color = color != null ? color : LLRBNode.RED;
- this.left = left != null ? left : LLRBNode.EMPTY;
- this.right = right != null ? right : LLRBNode.EMPTY;
- this.size = this.left.size + 1 + this.right.size;
- }
- // Returns a copy of the current node, optionally replacing pieces of it.
- copy(key, value, color, left, right) {
- return new LLRBNode(key != null ? key : this.key, value != null ? value : this.value, color != null ? color : this.color, left != null ? left : this.left, right != null ? right : this.right);
- }
- isEmpty() {
- return false;
- }
- // Traverses the tree in key order and calls the specified action function
- // for each node. If action returns true, traversal is aborted.
- // Returns the first truthy value returned by action, or the last falsey
- // value returned by action.
- inorderTraversal(action) {
- return (this.left.inorderTraversal(action) ||
- action(this.key, this.value) ||
- this.right.inorderTraversal(action));
- }
- // Traverses the tree in reverse key order and calls the specified action
- // function for each node. If action returns true, traversal is aborted.
- // Returns the first truthy value returned by action, or the last falsey
- // value returned by action.
- reverseTraversal(action) {
- return (this.right.reverseTraversal(action) ||
- action(this.key, this.value) ||
- this.left.reverseTraversal(action));
- }
- // Returns the minimum node in the tree.
- min() {
- if (this.left.isEmpty()) {
- return this;
- }
- else {
- return this.left.min();
- }
- }
- // Returns the maximum key in the tree.
- minKey() {
- return this.min().key;
- }
- // Returns the maximum key in the tree.
- maxKey() {
- if (this.right.isEmpty()) {
- return this.key;
- }
- else {
- return this.right.maxKey();
- }
- }
- // Returns new tree, with the key/value added.
- insert(key, value, comparator) {
- let n = this;
- const cmp = comparator(key, n.key);
- if (cmp < 0) {
- n = n.copy(null, null, null, n.left.insert(key, value, comparator), null);
- }
- else if (cmp === 0) {
- n = n.copy(null, value, null, null, null);
- }
- else {
- n = n.copy(null, null, null, null, n.right.insert(key, value, comparator));
- }
- return n.fixUp();
- }
- removeMin() {
- if (this.left.isEmpty()) {
- return LLRBNode.EMPTY;
- }
- let n = this;
- if (!n.left.isRed() && !n.left.left.isRed()) {
- n = n.moveRedLeft();
- }
- n = n.copy(null, null, null, n.left.removeMin(), null);
- return n.fixUp();
- }
- // Returns new tree, with the specified item removed.
- remove(key, comparator) {
- let smallest;
- let n = this;
- if (comparator(key, n.key) < 0) {
- if (!n.left.isEmpty() && !n.left.isRed() && !n.left.left.isRed()) {
- n = n.moveRedLeft();
- }
- n = n.copy(null, null, null, n.left.remove(key, comparator), null);
- }
- else {
- if (n.left.isRed()) {
- n = n.rotateRight();
- }
- if (!n.right.isEmpty() && !n.right.isRed() && !n.right.left.isRed()) {
- n = n.moveRedRight();
- }
- if (comparator(key, n.key) === 0) {
- if (n.right.isEmpty()) {
- return LLRBNode.EMPTY;
- }
- else {
- smallest = n.right.min();
- n = n.copy(smallest.key, smallest.value, null, null, n.right.removeMin());
- }
- }
- n = n.copy(null, null, null, null, n.right.remove(key, comparator));
- }
- return n.fixUp();
- }
- isRed() {
- return this.color;
- }
- // Returns new tree after performing any needed rotations.
- fixUp() {
- let n = this;
- if (n.right.isRed() && !n.left.isRed()) {
- n = n.rotateLeft();
- }
- if (n.left.isRed() && n.left.left.isRed()) {
- n = n.rotateRight();
- }
- if (n.left.isRed() && n.right.isRed()) {
- n = n.colorFlip();
- }
- return n;
- }
- moveRedLeft() {
- let n = this.colorFlip();
- if (n.right.left.isRed()) {
- n = n.copy(null, null, null, null, n.right.rotateRight());
- n = n.rotateLeft();
- n = n.colorFlip();
- }
- return n;
- }
- moveRedRight() {
- let n = this.colorFlip();
- if (n.left.left.isRed()) {
- n = n.rotateRight();
- n = n.colorFlip();
- }
- return n;
- }
- rotateLeft() {
- const nl = this.copy(null, null, LLRBNode.RED, null, this.right.left);
- return this.right.copy(null, null, this.color, nl, null);
- }
- rotateRight() {
- const nr = this.copy(null, null, LLRBNode.RED, this.left.right, null);
- return this.left.copy(null, null, this.color, null, nr);
- }
- colorFlip() {
- const left = this.left.copy(null, null, !this.left.color, null, null);
- const right = this.right.copy(null, null, !this.right.color, null, null);
- return this.copy(null, null, !this.color, left, right);
- }
- // For testing.
- checkMaxDepth() {
- const blackDepth = this.check();
- if (Math.pow(2.0, blackDepth) <= this.size + 1) {
- return true;
- }
- else {
- return false;
- }
- }
- // In a balanced RB tree, the black-depth (number of black nodes) from root to
- // leaves is equal on both sides. This function verifies that or asserts.
- check() {
- if (this.isRed() && this.left.isRed()) {
- throw fail();
- }
- if (this.right.isRed()) {
- throw fail();
- }
- const blackDepth = this.left.check();
- if (blackDepth !== this.right.check()) {
- throw fail();
- }
- else {
- return blackDepth + (this.isRed() ? 0 : 1);
- }
- }
- } // end LLRBNode
- // Empty node is shared between all LLRB trees.
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- LLRBNode.EMPTY = null;
- LLRBNode.RED = true;
- LLRBNode.BLACK = false;
- // Represents an empty node (a leaf node in the Red-Black Tree).
- class LLRBEmptyNode {
- constructor() {
- this.size = 0;
- }
- get key() {
- throw fail();
- }
- get value() {
- throw fail();
- }
- get color() {
- throw fail();
- }
- get left() {
- throw fail();
- }
- get right() {
- throw fail();
- }
- // Returns a copy of the current node.
- copy(key, value, color, left, right) {
- return this;
- }
- // Returns a copy of the tree, with the specified key/value added.
- insert(key, value, comparator) {
- return new LLRBNode(key, value);
- }
- // Returns a copy of the tree, with the specified key removed.
- remove(key, comparator) {
- return this;
- }
- isEmpty() {
- return true;
- }
- inorderTraversal(action) {
- return false;
- }
- reverseTraversal(action) {
- return false;
- }
- minKey() {
- return null;
- }
- maxKey() {
- return null;
- }
- isRed() {
- return false;
- }
- // For testing.
- checkMaxDepth() {
- return true;
- }
- check() {
- return 0;
- }
- } // end LLRBEmptyNode
- LLRBNode.EMPTY = new LLRBEmptyNode();
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * SortedSet is an immutable (copy-on-write) collection that holds elements
- * in order specified by the provided comparator.
- *
- * NOTE: if provided comparator returns 0 for two elements, we consider them to
- * be equal!
- */
- class SortedSet {
- constructor(comparator) {
- this.comparator = comparator;
- this.data = new SortedMap(this.comparator);
- }
- has(elem) {
- return this.data.get(elem) !== null;
- }
- first() {
- return this.data.minKey();
- }
- last() {
- return this.data.maxKey();
- }
- get size() {
- return this.data.size;
- }
- indexOf(elem) {
- return this.data.indexOf(elem);
- }
- /** Iterates elements in order defined by "comparator" */
- forEach(cb) {
- this.data.inorderTraversal((k, v) => {
- cb(k);
- return false;
- });
- }
- /** Iterates over `elem`s such that: range[0] <= elem < range[1]. */
- forEachInRange(range, cb) {
- const iter = this.data.getIteratorFrom(range[0]);
- while (iter.hasNext()) {
- const elem = iter.getNext();
- if (this.comparator(elem.key, range[1]) >= 0) {
- return;
- }
- cb(elem.key);
- }
- }
- /**
- * Iterates over `elem`s such that: start <= elem until false is returned.
- */
- forEachWhile(cb, start) {
- let iter;
- if (start !== undefined) {
- iter = this.data.getIteratorFrom(start);
- }
- else {
- iter = this.data.getIterator();
- }
- while (iter.hasNext()) {
- const elem = iter.getNext();
- const result = cb(elem.key);
- if (!result) {
- return;
- }
- }
- }
- /** Finds the least element greater than or equal to `elem`. */
- firstAfterOrEqual(elem) {
- const iter = this.data.getIteratorFrom(elem);
- return iter.hasNext() ? iter.getNext().key : null;
- }
- getIterator() {
- return new SortedSetIterator(this.data.getIterator());
- }
- getIteratorFrom(key) {
- return new SortedSetIterator(this.data.getIteratorFrom(key));
- }
- /** Inserts or updates an element */
- add(elem) {
- return this.copy(this.data.remove(elem).insert(elem, true));
- }
- /** Deletes an element */
- delete(elem) {
- if (!this.has(elem)) {
- return this;
- }
- return this.copy(this.data.remove(elem));
- }
- isEmpty() {
- return this.data.isEmpty();
- }
- unionWith(other) {
- let result = this;
- // Make sure `result` always refers to the larger one of the two sets.
- if (result.size < other.size) {
- result = other;
- other = this;
- }
- other.forEach(elem => {
- result = result.add(elem);
- });
- return result;
- }
- isEqual(other) {
- if (!(other instanceof SortedSet)) {
- return false;
- }
- if (this.size !== other.size) {
- return false;
- }
- const thisIt = this.data.getIterator();
- const otherIt = other.data.getIterator();
- while (thisIt.hasNext()) {
- const thisElem = thisIt.getNext().key;
- const otherElem = otherIt.getNext().key;
- if (this.comparator(thisElem, otherElem) !== 0) {
- return false;
- }
- }
- return true;
- }
- toArray() {
- const res = [];
- this.forEach(targetId => {
- res.push(targetId);
- });
- return res;
- }
- toString() {
- const result = [];
- this.forEach(elem => result.push(elem));
- return 'SortedSet(' + result.toString() + ')';
- }
- copy(data) {
- const result = new SortedSet(this.comparator);
- result.data = data;
- return result;
- }
- }
- class SortedSetIterator {
- constructor(iter) {
- this.iter = iter;
- }
- getNext() {
- return this.iter.getNext().key;
- }
- hasNext() {
- return this.iter.hasNext();
- }
- }
- /**
- * Compares two sorted sets for equality using their natural ordering. The
- * method computes the intersection and invokes `onAdd` for every element that
- * is in `after` but not `before`. `onRemove` is invoked for every element in
- * `before` but missing from `after`.
- *
- * The method creates a copy of both `before` and `after` and runs in O(n log
- * n), where n is the size of the two lists.
- *
- * @param before - The elements that exist in the original set.
- * @param after - The elements to diff against the original set.
- * @param comparator - The comparator for the elements in before and after.
- * @param onAdd - A function to invoke for every element that is part of `
- * after` but not `before`.
- * @param onRemove - A function to invoke for every element that is part of
- * `before` but not `after`.
- */
- function diffSortedSets(before, after, comparator, onAdd, onRemove) {
- const beforeIt = before.getIterator();
- const afterIt = after.getIterator();
- let beforeValue = advanceIterator(beforeIt);
- let afterValue = advanceIterator(afterIt);
- // Walk through the two sets at the same time, using the ordering defined by
- // `comparator`.
- while (beforeValue || afterValue) {
- let added = false;
- let removed = false;
- if (beforeValue && afterValue) {
- const cmp = comparator(beforeValue, afterValue);
- if (cmp < 0) {
- // The element was removed if the next element in our ordered
- // walkthrough is only in `before`.
- removed = true;
- }
- else if (cmp > 0) {
- // The element was added if the next element in our ordered walkthrough
- // is only in `after`.
- added = true;
- }
- }
- else if (beforeValue != null) {
- removed = true;
- }
- else {
- added = true;
- }
- if (added) {
- onAdd(afterValue);
- afterValue = advanceIterator(afterIt);
- }
- else if (removed) {
- onRemove(beforeValue);
- beforeValue = advanceIterator(beforeIt);
- }
- else {
- beforeValue = advanceIterator(beforeIt);
- afterValue = advanceIterator(afterIt);
- }
- }
- }
- /**
- * Returns the next element from the iterator or `undefined` if none available.
- */
- function advanceIterator(it) {
- return it.hasNext() ? it.getNext() : undefined;
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Provides a set of fields that can be used to partially patch a document.
- * FieldMask is used in conjunction with ObjectValue.
- * Examples:
- * foo - Overwrites foo entirely with the provided value. If foo is not
- * present in the companion ObjectValue, the field is deleted.
- * foo.bar - Overwrites only the field bar of the object foo.
- * If foo is not an object, foo is replaced with an object
- * containing foo
- */
- class FieldMask {
- constructor(fields) {
- this.fields = fields;
- // TODO(dimond): validation of FieldMask
- // Sort the field mask to support `FieldMask.isEqual()` and assert below.
- fields.sort(FieldPath$1.comparator);
- }
- static empty() {
- return new FieldMask([]);
- }
- /**
- * Returns a new FieldMask object that is the result of adding all the given
- * fields paths to this field mask.
- */
- unionWith(extraFields) {
- let mergedMaskSet = new SortedSet(FieldPath$1.comparator);
- for (const fieldPath of this.fields) {
- mergedMaskSet = mergedMaskSet.add(fieldPath);
- }
- for (const fieldPath of extraFields) {
- mergedMaskSet = mergedMaskSet.add(fieldPath);
- }
- return new FieldMask(mergedMaskSet.toArray());
- }
- /**
- * Verifies that `fieldPath` is included by at least one field in this field
- * mask.
- *
- * This is an O(n) operation, where `n` is the size of the field mask.
- */
- covers(fieldPath) {
- for (const fieldMaskPath of this.fields) {
- if (fieldMaskPath.isPrefixOf(fieldPath)) {
- return true;
- }
- }
- return false;
- }
- isEqual(other) {
- return arrayEquals(this.fields, other.fields, (l, r) => l.isEqual(r));
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /** Converts a Base64 encoded string to a binary string. */
- function decodeBase64(encoded) {
- // Note: We used to validate the base64 string here via a regular expression.
- // This was removed to improve the performance of indexing.
- return Buffer.from(encoded, 'base64').toString('binary');
- }
- /** Converts a binary string to a Base64 encoded string. */
- function encodeBase64(raw) {
- return Buffer.from(raw, 'binary').toString('base64');
- }
- /** True if and only if the Base64 conversion functions are available. */
- function isBase64Available() {
- return true;
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Immutable class that represents a "proto" byte string.
- *
- * Proto byte strings can either be Base64-encoded strings or Uint8Arrays when
- * sent on the wire. This class abstracts away this differentiation by holding
- * the proto byte string in a common class that must be converted into a string
- * before being sent as a proto.
- * @internal
- */
- class ByteString {
- constructor(binaryString) {
- this.binaryString = binaryString;
- }
- static fromBase64String(base64) {
- const binaryString = decodeBase64(base64);
- return new ByteString(binaryString);
- }
- static fromUint8Array(array) {
- // TODO(indexing); Remove the copy of the byte string here as this method
- // is frequently called during indexing.
- const binaryString = binaryStringFromUint8Array(array);
- return new ByteString(binaryString);
- }
- [Symbol.iterator]() {
- let i = 0;
- return {
- next: () => {
- if (i < this.binaryString.length) {
- return { value: this.binaryString.charCodeAt(i++), done: false };
- }
- else {
- return { value: undefined, done: true };
- }
- }
- };
- }
- toBase64() {
- return encodeBase64(this.binaryString);
- }
- toUint8Array() {
- return uint8ArrayFromBinaryString(this.binaryString);
- }
- approximateByteSize() {
- return this.binaryString.length * 2;
- }
- compareTo(other) {
- return primitiveComparator(this.binaryString, other.binaryString);
- }
- isEqual(other) {
- return this.binaryString === other.binaryString;
- }
- }
- ByteString.EMPTY_BYTE_STRING = new ByteString('');
- /**
- * Helper function to convert an Uint8array to a binary string.
- */
- function binaryStringFromUint8Array(array) {
- let binaryString = '';
- for (let i = 0; i < array.length; ++i) {
- binaryString += String.fromCharCode(array[i]);
- }
- return binaryString;
- }
- /**
- * Helper function to convert a binary string to an Uint8Array.
- */
- function uint8ArrayFromBinaryString(binaryString) {
- const buffer = new Uint8Array(binaryString.length);
- for (let i = 0; i < binaryString.length; i++) {
- buffer[i] = binaryString.charCodeAt(i);
- }
- return buffer;
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- // A RegExp matching ISO 8601 UTC timestamps with optional fraction.
- const ISO_TIMESTAMP_REG_EXP = new RegExp(/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.(\d+))?Z$/);
- /**
- * Converts the possible Proto values for a timestamp value into a "seconds and
- * nanos" representation.
- */
- function normalizeTimestamp(date) {
- hardAssert(!!date);
- // The json interface (for the browser) will return an iso timestamp string,
- // while the proto js library (for node) will return a
- // google.protobuf.Timestamp instance.
- if (typeof date === 'string') {
- // The date string can have higher precision (nanos) than the Date class
- // (millis), so we do some custom parsing here.
- // Parse the nanos right out of the string.
- let nanos = 0;
- const fraction = ISO_TIMESTAMP_REG_EXP.exec(date);
- hardAssert(!!fraction);
- if (fraction[1]) {
- // Pad the fraction out to 9 digits (nanos).
- let nanoStr = fraction[1];
- nanoStr = (nanoStr + '000000000').substr(0, 9);
- nanos = Number(nanoStr);
- }
- // Parse the date to get the seconds.
- const parsedDate = new Date(date);
- const seconds = Math.floor(parsedDate.getTime() / 1000);
- return { seconds, nanos };
- }
- else {
- // TODO(b/37282237): Use strings for Proto3 timestamps
- // assert(!this.options.useProto3Json,
- // 'The timestamp instance format requires Proto JS.');
- const seconds = normalizeNumber(date.seconds);
- const nanos = normalizeNumber(date.nanos);
- return { seconds, nanos };
- }
- }
- /**
- * Converts the possible Proto types for numbers into a JavaScript number.
- * Returns 0 if the value is not numeric.
- */
- function normalizeNumber(value) {
- // TODO(bjornick): Handle int64 greater than 53 bits.
- if (typeof value === 'number') {
- return value;
- }
- else if (typeof value === 'string') {
- return Number(value);
- }
- else {
- return 0;
- }
- }
- /** Converts the possible Proto types for Blobs into a ByteString. */
- function normalizeByteString(blob) {
- if (typeof blob === 'string') {
- return ByteString.fromBase64String(blob);
- }
- else {
- return ByteString.fromUint8Array(blob);
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Represents a locally-applied ServerTimestamp.
- *
- * Server Timestamps are backed by MapValues that contain an internal field
- * `__type__` with a value of `server_timestamp`. The previous value and local
- * write time are stored in its `__previous_value__` and `__local_write_time__`
- * fields respectively.
- *
- * Notes:
- * - ServerTimestampValue instances are created as the result of applying a
- * transform. They can only exist in the local view of a document. Therefore
- * they do not need to be parsed or serialized.
- * - When evaluated locally (e.g. for snapshot.data()), they by default
- * evaluate to `null`. This behavior can be configured by passing custom
- * FieldValueOptions to value().
- * - With respect to other ServerTimestampValues, they sort by their
- * localWriteTime.
- */
- const SERVER_TIMESTAMP_SENTINEL = 'server_timestamp';
- const TYPE_KEY = '__type__';
- const PREVIOUS_VALUE_KEY = '__previous_value__';
- const LOCAL_WRITE_TIME_KEY = '__local_write_time__';
- function isServerTimestamp(value) {
- var _a, _b;
- const type = (_b = (((_a = value === null || value === void 0 ? void 0 : value.mapValue) === null || _a === void 0 ? void 0 : _a.fields) || {})[TYPE_KEY]) === null || _b === void 0 ? void 0 : _b.stringValue;
- return type === SERVER_TIMESTAMP_SENTINEL;
- }
- /**
- * Creates a new ServerTimestamp proto value (using the internal format).
- */
- function serverTimestamp$1(localWriteTime, previousValue) {
- const mapValue = {
- fields: {
- [TYPE_KEY]: {
- stringValue: SERVER_TIMESTAMP_SENTINEL
- },
- [LOCAL_WRITE_TIME_KEY]: {
- timestampValue: {
- seconds: localWriteTime.seconds,
- nanos: localWriteTime.nanoseconds
- }
- }
- }
- };
- // We should avoid storing deeply nested server timestamp map values
- // because we never use the intermediate "previous values".
- // For example:
- // previous: 42L, add: t1, result: t1 -> 42L
- // previous: t1, add: t2, result: t2 -> 42L (NOT t2 -> t1 -> 42L)
- // previous: t2, add: t3, result: t3 -> 42L (NOT t3 -> t2 -> t1 -> 42L)
- // `getPreviousValue` recursively traverses server timestamps to find the
- // least recent Value.
- if (previousValue && isServerTimestamp(previousValue)) {
- previousValue = getPreviousValue(previousValue);
- }
- if (previousValue) {
- mapValue.fields[PREVIOUS_VALUE_KEY] = previousValue;
- }
- return { mapValue };
- }
- /**
- * Returns the value of the field before this ServerTimestamp was set.
- *
- * Preserving the previous values allows the user to display the last resoled
- * value until the backend responds with the timestamp.
- */
- function getPreviousValue(value) {
- const previousValue = value.mapValue.fields[PREVIOUS_VALUE_KEY];
- if (isServerTimestamp(previousValue)) {
- return getPreviousValue(previousValue);
- }
- return previousValue;
- }
- /**
- * Returns the local time at which this timestamp was first set.
- */
- function getLocalWriteTime(value) {
- const localWriteTime = normalizeTimestamp(value.mapValue.fields[LOCAL_WRITE_TIME_KEY].timestampValue);
- return new Timestamp(localWriteTime.seconds, localWriteTime.nanos);
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- class DatabaseInfo {
- /**
- * Constructs a DatabaseInfo using the provided host, databaseId and
- * persistenceKey.
- *
- * @param databaseId - The database to use.
- * @param appId - The Firebase App Id.
- * @param persistenceKey - A unique identifier for this Firestore's local
- * storage (used in conjunction with the databaseId).
- * @param host - The Firestore backend host to connect to.
- * @param ssl - Whether to use SSL when connecting.
- * @param forceLongPolling - Whether to use the forceLongPolling option
- * when using WebChannel as the network transport.
- * @param autoDetectLongPolling - Whether to use the detectBufferingProxy
- * option when using WebChannel as the network transport.
- * @param longPollingOptions Options that configure long-polling.
- * @param useFetchStreams Whether to use the Fetch API instead of
- * XMLHTTPRequest
- */
- constructor(databaseId, appId, persistenceKey, host, ssl, forceLongPolling, autoDetectLongPolling, longPollingOptions, useFetchStreams) {
- this.databaseId = databaseId;
- this.appId = appId;
- this.persistenceKey = persistenceKey;
- this.host = host;
- this.ssl = ssl;
- this.forceLongPolling = forceLongPolling;
- this.autoDetectLongPolling = autoDetectLongPolling;
- this.longPollingOptions = longPollingOptions;
- this.useFetchStreams = useFetchStreams;
- }
- }
- /** The default database name for a project. */
- const DEFAULT_DATABASE_NAME = '(default)';
- /**
- * Represents the database ID a Firestore client is associated with.
- * @internal
- */
- class DatabaseId {
- constructor(projectId, database) {
- this.projectId = projectId;
- this.database = database ? database : DEFAULT_DATABASE_NAME;
- }
- static empty() {
- return new DatabaseId('', '');
- }
- get isDefaultDatabase() {
- return this.database === DEFAULT_DATABASE_NAME;
- }
- isEqual(other) {
- return (other instanceof DatabaseId &&
- other.projectId === this.projectId &&
- other.database === this.database);
- }
- }
- function databaseIdFromApp(app, database) {
- if (!Object.prototype.hasOwnProperty.apply(app.options, ['projectId'])) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, '"projectId" not provided in firebase.initializeApp.');
- }
- return new DatabaseId(app.options.projectId, database);
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /** Sentinel value that sorts before any Mutation Batch ID. */
- const BATCHID_UNKNOWN = -1;
- /**
- * Returns whether a variable is either undefined or null.
- */
- function isNullOrUndefined(value) {
- return value === null || value === undefined;
- }
- /** Returns whether the value represents -0. */
- function isNegativeZero(value) {
- // Detect if the value is -0.0. Based on polyfill from
- // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
- return value === 0 && 1 / value === 1 / -0;
- }
- /**
- * Returns whether a value is an integer and in the safe integer range
- * @param value - The value to test for being an integer and in the safe range
- */
- function isSafeInteger(value) {
- return (typeof value === 'number' &&
- Number.isInteger(value) &&
- !isNegativeZero(value) &&
- value <= Number.MAX_SAFE_INTEGER &&
- value >= Number.MIN_SAFE_INTEGER);
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const MAX_VALUE_TYPE = '__max__';
- const MAX_VALUE = {
- mapValue: {
- fields: {
- '__type__': { stringValue: MAX_VALUE_TYPE }
- }
- }
- };
- const MIN_VALUE = {
- nullValue: 'NULL_VALUE'
- };
- /** Extracts the backend's type order for the provided value. */
- function typeOrder(value) {
- if ('nullValue' in value) {
- return 0 /* TypeOrder.NullValue */;
- }
- else if ('booleanValue' in value) {
- return 1 /* TypeOrder.BooleanValue */;
- }
- else if ('integerValue' in value || 'doubleValue' in value) {
- return 2 /* TypeOrder.NumberValue */;
- }
- else if ('timestampValue' in value) {
- return 3 /* TypeOrder.TimestampValue */;
- }
- else if ('stringValue' in value) {
- return 5 /* TypeOrder.StringValue */;
- }
- else if ('bytesValue' in value) {
- return 6 /* TypeOrder.BlobValue */;
- }
- else if ('referenceValue' in value) {
- return 7 /* TypeOrder.RefValue */;
- }
- else if ('geoPointValue' in value) {
- return 8 /* TypeOrder.GeoPointValue */;
- }
- else if ('arrayValue' in value) {
- return 9 /* TypeOrder.ArrayValue */;
- }
- else if ('mapValue' in value) {
- if (isServerTimestamp(value)) {
- return 4 /* TypeOrder.ServerTimestampValue */;
- }
- else if (isMaxValue(value)) {
- return 9007199254740991 /* TypeOrder.MaxValue */;
- }
- return 10 /* TypeOrder.ObjectValue */;
- }
- else {
- return fail();
- }
- }
- /** Tests `left` and `right` for equality based on the backend semantics. */
- function valueEquals(left, right) {
- if (left === right) {
- return true;
- }
- const leftType = typeOrder(left);
- const rightType = typeOrder(right);
- if (leftType !== rightType) {
- return false;
- }
- switch (leftType) {
- case 0 /* TypeOrder.NullValue */:
- return true;
- case 1 /* TypeOrder.BooleanValue */:
- return left.booleanValue === right.booleanValue;
- case 4 /* TypeOrder.ServerTimestampValue */:
- return getLocalWriteTime(left).isEqual(getLocalWriteTime(right));
- case 3 /* TypeOrder.TimestampValue */:
- return timestampEquals(left, right);
- case 5 /* TypeOrder.StringValue */:
- return left.stringValue === right.stringValue;
- case 6 /* TypeOrder.BlobValue */:
- return blobEquals(left, right);
- case 7 /* TypeOrder.RefValue */:
- return left.referenceValue === right.referenceValue;
- case 8 /* TypeOrder.GeoPointValue */:
- return geoPointEquals(left, right);
- case 2 /* TypeOrder.NumberValue */:
- return numberEquals(left, right);
- case 9 /* TypeOrder.ArrayValue */:
- return arrayEquals(left.arrayValue.values || [], right.arrayValue.values || [], valueEquals);
- case 10 /* TypeOrder.ObjectValue */:
- return objectEquals(left, right);
- case 9007199254740991 /* TypeOrder.MaxValue */:
- return true;
- default:
- return fail();
- }
- }
- function timestampEquals(left, right) {
- if (typeof left.timestampValue === 'string' &&
- typeof right.timestampValue === 'string' &&
- left.timestampValue.length === right.timestampValue.length) {
- // Use string equality for ISO 8601 timestamps
- return left.timestampValue === right.timestampValue;
- }
- const leftTimestamp = normalizeTimestamp(left.timestampValue);
- const rightTimestamp = normalizeTimestamp(right.timestampValue);
- return (leftTimestamp.seconds === rightTimestamp.seconds &&
- leftTimestamp.nanos === rightTimestamp.nanos);
- }
- function geoPointEquals(left, right) {
- return (normalizeNumber(left.geoPointValue.latitude) ===
- normalizeNumber(right.geoPointValue.latitude) &&
- normalizeNumber(left.geoPointValue.longitude) ===
- normalizeNumber(right.geoPointValue.longitude));
- }
- function blobEquals(left, right) {
- return normalizeByteString(left.bytesValue).isEqual(normalizeByteString(right.bytesValue));
- }
- function numberEquals(left, right) {
- if ('integerValue' in left && 'integerValue' in right) {
- return (normalizeNumber(left.integerValue) === normalizeNumber(right.integerValue));
- }
- else if ('doubleValue' in left && 'doubleValue' in right) {
- const n1 = normalizeNumber(left.doubleValue);
- const n2 = normalizeNumber(right.doubleValue);
- if (n1 === n2) {
- return isNegativeZero(n1) === isNegativeZero(n2);
- }
- else {
- return isNaN(n1) && isNaN(n2);
- }
- }
- return false;
- }
- function objectEquals(left, right) {
- const leftMap = left.mapValue.fields || {};
- const rightMap = right.mapValue.fields || {};
- if (objectSize(leftMap) !== objectSize(rightMap)) {
- return false;
- }
- for (const key in leftMap) {
- if (leftMap.hasOwnProperty(key)) {
- if (rightMap[key] === undefined ||
- !valueEquals(leftMap[key], rightMap[key])) {
- return false;
- }
- }
- }
- return true;
- }
- /** Returns true if the ArrayValue contains the specified element. */
- function arrayValueContains(haystack, needle) {
- return ((haystack.values || []).find(v => valueEquals(v, needle)) !== undefined);
- }
- function valueCompare(left, right) {
- if (left === right) {
- return 0;
- }
- const leftType = typeOrder(left);
- const rightType = typeOrder(right);
- if (leftType !== rightType) {
- return primitiveComparator(leftType, rightType);
- }
- switch (leftType) {
- case 0 /* TypeOrder.NullValue */:
- case 9007199254740991 /* TypeOrder.MaxValue */:
- return 0;
- case 1 /* TypeOrder.BooleanValue */:
- return primitiveComparator(left.booleanValue, right.booleanValue);
- case 2 /* TypeOrder.NumberValue */:
- return compareNumbers(left, right);
- case 3 /* TypeOrder.TimestampValue */:
- return compareTimestamps(left.timestampValue, right.timestampValue);
- case 4 /* TypeOrder.ServerTimestampValue */:
- return compareTimestamps(getLocalWriteTime(left), getLocalWriteTime(right));
- case 5 /* TypeOrder.StringValue */:
- return primitiveComparator(left.stringValue, right.stringValue);
- case 6 /* TypeOrder.BlobValue */:
- return compareBlobs(left.bytesValue, right.bytesValue);
- case 7 /* TypeOrder.RefValue */:
- return compareReferences(left.referenceValue, right.referenceValue);
- case 8 /* TypeOrder.GeoPointValue */:
- return compareGeoPoints(left.geoPointValue, right.geoPointValue);
- case 9 /* TypeOrder.ArrayValue */:
- return compareArrays(left.arrayValue, right.arrayValue);
- case 10 /* TypeOrder.ObjectValue */:
- return compareMaps(left.mapValue, right.mapValue);
- default:
- throw fail();
- }
- }
- function compareNumbers(left, right) {
- const leftNumber = normalizeNumber(left.integerValue || left.doubleValue);
- const rightNumber = normalizeNumber(right.integerValue || right.doubleValue);
- if (leftNumber < rightNumber) {
- return -1;
- }
- else if (leftNumber > rightNumber) {
- return 1;
- }
- else if (leftNumber === rightNumber) {
- return 0;
- }
- else {
- // one or both are NaN.
- if (isNaN(leftNumber)) {
- return isNaN(rightNumber) ? 0 : -1;
- }
- else {
- return 1;
- }
- }
- }
- function compareTimestamps(left, right) {
- if (typeof left === 'string' &&
- typeof right === 'string' &&
- left.length === right.length) {
- return primitiveComparator(left, right);
- }
- const leftTimestamp = normalizeTimestamp(left);
- const rightTimestamp = normalizeTimestamp(right);
- const comparison = primitiveComparator(leftTimestamp.seconds, rightTimestamp.seconds);
- if (comparison !== 0) {
- return comparison;
- }
- return primitiveComparator(leftTimestamp.nanos, rightTimestamp.nanos);
- }
- function compareReferences(leftPath, rightPath) {
- const leftSegments = leftPath.split('/');
- const rightSegments = rightPath.split('/');
- for (let i = 0; i < leftSegments.length && i < rightSegments.length; i++) {
- const comparison = primitiveComparator(leftSegments[i], rightSegments[i]);
- if (comparison !== 0) {
- return comparison;
- }
- }
- return primitiveComparator(leftSegments.length, rightSegments.length);
- }
- function compareGeoPoints(left, right) {
- const comparison = primitiveComparator(normalizeNumber(left.latitude), normalizeNumber(right.latitude));
- if (comparison !== 0) {
- return comparison;
- }
- return primitiveComparator(normalizeNumber(left.longitude), normalizeNumber(right.longitude));
- }
- function compareBlobs(left, right) {
- const leftBytes = normalizeByteString(left);
- const rightBytes = normalizeByteString(right);
- return leftBytes.compareTo(rightBytes);
- }
- function compareArrays(left, right) {
- const leftArray = left.values || [];
- const rightArray = right.values || [];
- for (let i = 0; i < leftArray.length && i < rightArray.length; ++i) {
- const compare = valueCompare(leftArray[i], rightArray[i]);
- if (compare) {
- return compare;
- }
- }
- return primitiveComparator(leftArray.length, rightArray.length);
- }
- function compareMaps(left, right) {
- if (left === MAX_VALUE.mapValue && right === MAX_VALUE.mapValue) {
- return 0;
- }
- else if (left === MAX_VALUE.mapValue) {
- return 1;
- }
- else if (right === MAX_VALUE.mapValue) {
- return -1;
- }
- const leftMap = left.fields || {};
- const leftKeys = Object.keys(leftMap);
- const rightMap = right.fields || {};
- const rightKeys = Object.keys(rightMap);
- // Even though MapValues are likely sorted correctly based on their insertion
- // order (e.g. when received from the backend), local modifications can bring
- // elements out of order. We need to re-sort the elements to ensure that
- // canonical IDs are independent of insertion order.
- leftKeys.sort();
- rightKeys.sort();
- for (let i = 0; i < leftKeys.length && i < rightKeys.length; ++i) {
- const keyCompare = primitiveComparator(leftKeys[i], rightKeys[i]);
- if (keyCompare !== 0) {
- return keyCompare;
- }
- const compare = valueCompare(leftMap[leftKeys[i]], rightMap[rightKeys[i]]);
- if (compare !== 0) {
- return compare;
- }
- }
- return primitiveComparator(leftKeys.length, rightKeys.length);
- }
- /**
- * Generates the canonical ID for the provided field value (as used in Target
- * serialization).
- */
- function canonicalId(value) {
- return canonifyValue(value);
- }
- function canonifyValue(value) {
- if ('nullValue' in value) {
- return 'null';
- }
- else if ('booleanValue' in value) {
- return '' + value.booleanValue;
- }
- else if ('integerValue' in value) {
- return '' + value.integerValue;
- }
- else if ('doubleValue' in value) {
- return '' + value.doubleValue;
- }
- else if ('timestampValue' in value) {
- return canonifyTimestamp(value.timestampValue);
- }
- else if ('stringValue' in value) {
- return value.stringValue;
- }
- else if ('bytesValue' in value) {
- return canonifyByteString(value.bytesValue);
- }
- else if ('referenceValue' in value) {
- return canonifyReference(value.referenceValue);
- }
- else if ('geoPointValue' in value) {
- return canonifyGeoPoint(value.geoPointValue);
- }
- else if ('arrayValue' in value) {
- return canonifyArray(value.arrayValue);
- }
- else if ('mapValue' in value) {
- return canonifyMap(value.mapValue);
- }
- else {
- return fail();
- }
- }
- function canonifyByteString(byteString) {
- return normalizeByteString(byteString).toBase64();
- }
- function canonifyTimestamp(timestamp) {
- const normalizedTimestamp = normalizeTimestamp(timestamp);
- return `time(${normalizedTimestamp.seconds},${normalizedTimestamp.nanos})`;
- }
- function canonifyGeoPoint(geoPoint) {
- return `geo(${geoPoint.latitude},${geoPoint.longitude})`;
- }
- function canonifyReference(referenceValue) {
- return DocumentKey.fromName(referenceValue).toString();
- }
- function canonifyMap(mapValue) {
- // Iteration order in JavaScript is not guaranteed. To ensure that we generate
- // matching canonical IDs for identical maps, we need to sort the keys.
- const sortedKeys = Object.keys(mapValue.fields || {}).sort();
- let result = '{';
- let first = true;
- for (const key of sortedKeys) {
- if (!first) {
- result += ',';
- }
- else {
- first = false;
- }
- result += `${key}:${canonifyValue(mapValue.fields[key])}`;
- }
- return result + '}';
- }
- function canonifyArray(arrayValue) {
- let result = '[';
- let first = true;
- for (const value of arrayValue.values || []) {
- if (!first) {
- result += ',';
- }
- else {
- first = false;
- }
- result += canonifyValue(value);
- }
- return result + ']';
- }
- /**
- * Returns an approximate (and wildly inaccurate) in-memory size for the field
- * value.
- *
- * The memory size takes into account only the actual user data as it resides
- * in memory and ignores object overhead.
- */
- function estimateByteSize(value) {
- switch (typeOrder(value)) {
- case 0 /* TypeOrder.NullValue */:
- return 4;
- case 1 /* TypeOrder.BooleanValue */:
- return 4;
- case 2 /* TypeOrder.NumberValue */:
- return 8;
- case 3 /* TypeOrder.TimestampValue */:
- // Timestamps are made up of two distinct numbers (seconds + nanoseconds)
- return 16;
- case 4 /* TypeOrder.ServerTimestampValue */:
- const previousValue = getPreviousValue(value);
- return previousValue ? 16 + estimateByteSize(previousValue) : 16;
- case 5 /* TypeOrder.StringValue */:
- // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures:
- // "JavaScript's String type is [...] a set of elements of 16-bit unsigned
- // integer values"
- return value.stringValue.length * 2;
- case 6 /* TypeOrder.BlobValue */:
- return normalizeByteString(value.bytesValue).approximateByteSize();
- case 7 /* TypeOrder.RefValue */:
- return value.referenceValue.length;
- case 8 /* TypeOrder.GeoPointValue */:
- // GeoPoints are made up of two distinct numbers (latitude + longitude)
- return 16;
- case 9 /* TypeOrder.ArrayValue */:
- return estimateArrayByteSize(value.arrayValue);
- case 10 /* TypeOrder.ObjectValue */:
- return estimateMapByteSize(value.mapValue);
- default:
- throw fail();
- }
- }
- function estimateMapByteSize(mapValue) {
- let size = 0;
- forEach(mapValue.fields, (key, val) => {
- size += key.length + estimateByteSize(val);
- });
- return size;
- }
- function estimateArrayByteSize(arrayValue) {
- return (arrayValue.values || []).reduce((previousSize, value) => previousSize + estimateByteSize(value), 0);
- }
- /** Returns a reference value for the provided database and key. */
- function refValue(databaseId, key) {
- return {
- referenceValue: `projects/${databaseId.projectId}/databases/${databaseId.database}/documents/${key.path.canonicalString()}`
- };
- }
- /** Returns true if `value` is an IntegerValue . */
- function isInteger(value) {
- return !!value && 'integerValue' in value;
- }
- /** Returns true if `value` is a DoubleValue. */
- function isDouble(value) {
- return !!value && 'doubleValue' in value;
- }
- /** Returns true if `value` is either an IntegerValue or a DoubleValue. */
- function isNumber(value) {
- return isInteger(value) || isDouble(value);
- }
- /** Returns true if `value` is an ArrayValue. */
- function isArray(value) {
- return !!value && 'arrayValue' in value;
- }
- /** Returns true if `value` is a NullValue. */
- function isNullValue(value) {
- return !!value && 'nullValue' in value;
- }
- /** Returns true if `value` is NaN. */
- function isNanValue(value) {
- return !!value && 'doubleValue' in value && isNaN(Number(value.doubleValue));
- }
- /** Returns true if `value` is a MapValue. */
- function isMapValue(value) {
- return !!value && 'mapValue' in value;
- }
- /** Creates a deep copy of `source`. */
- function deepClone(source) {
- if (source.geoPointValue) {
- return { geoPointValue: Object.assign({}, source.geoPointValue) };
- }
- else if (source.timestampValue &&
- typeof source.timestampValue === 'object') {
- return { timestampValue: Object.assign({}, source.timestampValue) };
- }
- else if (source.mapValue) {
- const target = { mapValue: { fields: {} } };
- forEach(source.mapValue.fields, (key, val) => (target.mapValue.fields[key] = deepClone(val)));
- return target;
- }
- else if (source.arrayValue) {
- const target = { arrayValue: { values: [] } };
- for (let i = 0; i < (source.arrayValue.values || []).length; ++i) {
- target.arrayValue.values[i] = deepClone(source.arrayValue.values[i]);
- }
- return target;
- }
- else {
- return Object.assign({}, source);
- }
- }
- /** Returns true if the Value represents the canonical {@link #MAX_VALUE} . */
- function isMaxValue(value) {
- return ((((value.mapValue || {}).fields || {})['__type__'] || {}).stringValue ===
- MAX_VALUE_TYPE);
- }
- /** Returns the lowest value for the given value type (inclusive). */
- function valuesGetLowerBound(value) {
- if ('nullValue' in value) {
- return MIN_VALUE;
- }
- else if ('booleanValue' in value) {
- return { booleanValue: false };
- }
- else if ('integerValue' in value || 'doubleValue' in value) {
- return { doubleValue: NaN };
- }
- else if ('timestampValue' in value) {
- return { timestampValue: { seconds: Number.MIN_SAFE_INTEGER } };
- }
- else if ('stringValue' in value) {
- return { stringValue: '' };
- }
- else if ('bytesValue' in value) {
- return { bytesValue: '' };
- }
- else if ('referenceValue' in value) {
- return refValue(DatabaseId.empty(), DocumentKey.empty());
- }
- else if ('geoPointValue' in value) {
- return { geoPointValue: { latitude: -90, longitude: -180 } };
- }
- else if ('arrayValue' in value) {
- return { arrayValue: {} };
- }
- else if ('mapValue' in value) {
- return { mapValue: {} };
- }
- else {
- return fail();
- }
- }
- /** Returns the largest value for the given value type (exclusive). */
- function valuesGetUpperBound(value) {
- if ('nullValue' in value) {
- return { booleanValue: false };
- }
- else if ('booleanValue' in value) {
- return { doubleValue: NaN };
- }
- else if ('integerValue' in value || 'doubleValue' in value) {
- return { timestampValue: { seconds: Number.MIN_SAFE_INTEGER } };
- }
- else if ('timestampValue' in value) {
- return { stringValue: '' };
- }
- else if ('stringValue' in value) {
- return { bytesValue: '' };
- }
- else if ('bytesValue' in value) {
- return refValue(DatabaseId.empty(), DocumentKey.empty());
- }
- else if ('referenceValue' in value) {
- return { geoPointValue: { latitude: -90, longitude: -180 } };
- }
- else if ('geoPointValue' in value) {
- return { arrayValue: {} };
- }
- else if ('arrayValue' in value) {
- return { mapValue: {} };
- }
- else if ('mapValue' in value) {
- return MAX_VALUE;
- }
- else {
- return fail();
- }
- }
- function lowerBoundCompare(left, right) {
- const cmp = valueCompare(left.value, right.value);
- if (cmp !== 0) {
- return cmp;
- }
- if (left.inclusive && !right.inclusive) {
- return -1;
- }
- else if (!left.inclusive && right.inclusive) {
- return 1;
- }
- return 0;
- }
- function upperBoundCompare(left, right) {
- const cmp = valueCompare(left.value, right.value);
- if (cmp !== 0) {
- return cmp;
- }
- if (left.inclusive && !right.inclusive) {
- return 1;
- }
- else if (!left.inclusive && right.inclusive) {
- return -1;
- }
- return 0;
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * An ObjectValue represents a MapValue in the Firestore Proto and offers the
- * ability to add and remove fields (via the ObjectValueBuilder).
- */
- class ObjectValue {
- constructor(value) {
- this.value = value;
- }
- static empty() {
- return new ObjectValue({ mapValue: {} });
- }
- /**
- * Returns the value at the given path or null.
- *
- * @param path - the path to search
- * @returns The value at the path or null if the path is not set.
- */
- field(path) {
- if (path.isEmpty()) {
- return this.value;
- }
- else {
- let currentLevel = this.value;
- for (let i = 0; i < path.length - 1; ++i) {
- currentLevel = (currentLevel.mapValue.fields || {})[path.get(i)];
- if (!isMapValue(currentLevel)) {
- return null;
- }
- }
- currentLevel = (currentLevel.mapValue.fields || {})[path.lastSegment()];
- return currentLevel || null;
- }
- }
- /**
- * Sets the field to the provided value.
- *
- * @param path - The field path to set.
- * @param value - The value to set.
- */
- set(path, value) {
- const fieldsMap = this.getFieldsMap(path.popLast());
- fieldsMap[path.lastSegment()] = deepClone(value);
- }
- /**
- * Sets the provided fields to the provided values.
- *
- * @param data - A map of fields to values (or null for deletes).
- */
- setAll(data) {
- let parent = FieldPath$1.emptyPath();
- let upserts = {};
- let deletes = [];
- data.forEach((value, path) => {
- if (!parent.isImmediateParentOf(path)) {
- // Insert the accumulated changes at this parent location
- const fieldsMap = this.getFieldsMap(parent);
- this.applyChanges(fieldsMap, upserts, deletes);
- upserts = {};
- deletes = [];
- parent = path.popLast();
- }
- if (value) {
- upserts[path.lastSegment()] = deepClone(value);
- }
- else {
- deletes.push(path.lastSegment());
- }
- });
- const fieldsMap = this.getFieldsMap(parent);
- this.applyChanges(fieldsMap, upserts, deletes);
- }
- /**
- * Removes the field at the specified path. If there is no field at the
- * specified path, nothing is changed.
- *
- * @param path - The field path to remove.
- */
- delete(path) {
- const nestedValue = this.field(path.popLast());
- if (isMapValue(nestedValue) && nestedValue.mapValue.fields) {
- delete nestedValue.mapValue.fields[path.lastSegment()];
- }
- }
- isEqual(other) {
- return valueEquals(this.value, other.value);
- }
- /**
- * Returns the map that contains the leaf element of `path`. If the parent
- * entry does not yet exist, or if it is not a map, a new map will be created.
- */
- getFieldsMap(path) {
- let current = this.value;
- if (!current.mapValue.fields) {
- current.mapValue = { fields: {} };
- }
- for (let i = 0; i < path.length; ++i) {
- let next = current.mapValue.fields[path.get(i)];
- if (!isMapValue(next) || !next.mapValue.fields) {
- next = { mapValue: { fields: {} } };
- current.mapValue.fields[path.get(i)] = next;
- }
- current = next;
- }
- return current.mapValue.fields;
- }
- /**
- * Modifies `fieldsMap` by adding, replacing or deleting the specified
- * entries.
- */
- applyChanges(fieldsMap, inserts, deletes) {
- forEach(inserts, (key, val) => (fieldsMap[key] = val));
- for (const field of deletes) {
- delete fieldsMap[field];
- }
- }
- clone() {
- return new ObjectValue(deepClone(this.value));
- }
- }
- /**
- * Returns a FieldMask built from all fields in a MapValue.
- */
- function extractFieldMask(value) {
- const fields = [];
- forEach(value.fields, (key, value) => {
- const currentPath = new FieldPath$1([key]);
- if (isMapValue(value)) {
- const nestedMask = extractFieldMask(value.mapValue);
- const nestedFields = nestedMask.fields;
- if (nestedFields.length === 0) {
- // Preserve the empty map by adding it to the FieldMask.
- fields.push(currentPath);
- }
- else {
- // For nested and non-empty ObjectValues, add the FieldPath of the
- // leaf nodes.
- for (const nestedPath of nestedFields) {
- fields.push(currentPath.child(nestedPath));
- }
- }
- }
- else {
- // For nested and non-empty ObjectValues, add the FieldPath of the leaf
- // nodes.
- fields.push(currentPath);
- }
- });
- return new FieldMask(fields);
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Represents a document in Firestore with a key, version, data and whether it
- * has local mutations applied to it.
- *
- * Documents can transition between states via `convertToFoundDocument()`,
- * `convertToNoDocument()` and `convertToUnknownDocument()`. If a document does
- * not transition to one of these states even after all mutations have been
- * applied, `isValidDocument()` returns false and the document should be removed
- * from all views.
- */
- class MutableDocument {
- constructor(key, documentType, version, readTime, createTime, data, documentState) {
- this.key = key;
- this.documentType = documentType;
- this.version = version;
- this.readTime = readTime;
- this.createTime = createTime;
- this.data = data;
- this.documentState = documentState;
- }
- /**
- * Creates a document with no known version or data, but which can serve as
- * base document for mutations.
- */
- static newInvalidDocument(documentKey) {
- return new MutableDocument(documentKey, 0 /* DocumentType.INVALID */,
- /* version */ SnapshotVersion.min(),
- /* readTime */ SnapshotVersion.min(),
- /* createTime */ SnapshotVersion.min(), ObjectValue.empty(), 0 /* DocumentState.SYNCED */);
- }
- /**
- * Creates a new document that is known to exist with the given data at the
- * given version.
- */
- static newFoundDocument(documentKey, version, createTime, value) {
- return new MutableDocument(documentKey, 1 /* DocumentType.FOUND_DOCUMENT */,
- /* version */ version,
- /* readTime */ SnapshotVersion.min(),
- /* createTime */ createTime, value, 0 /* DocumentState.SYNCED */);
- }
- /** Creates a new document that is known to not exist at the given version. */
- static newNoDocument(documentKey, version) {
- return new MutableDocument(documentKey, 2 /* DocumentType.NO_DOCUMENT */,
- /* version */ version,
- /* readTime */ SnapshotVersion.min(),
- /* createTime */ SnapshotVersion.min(), ObjectValue.empty(), 0 /* DocumentState.SYNCED */);
- }
- /**
- * Creates a new document that is known to exist at the given version but
- * whose data is not known (e.g. a document that was updated without a known
- * base document).
- */
- static newUnknownDocument(documentKey, version) {
- return new MutableDocument(documentKey, 3 /* DocumentType.UNKNOWN_DOCUMENT */,
- /* version */ version,
- /* readTime */ SnapshotVersion.min(),
- /* createTime */ SnapshotVersion.min(), ObjectValue.empty(), 2 /* DocumentState.HAS_COMMITTED_MUTATIONS */);
- }
- /**
- * Changes the document type to indicate that it exists and that its version
- * and data are known.
- */
- convertToFoundDocument(version, value) {
- // If a document is switching state from being an invalid or deleted
- // document to a valid (FOUND_DOCUMENT) document, either due to receiving an
- // update from Watch or due to applying a local set mutation on top
- // of a deleted document, our best guess about its createTime would be the
- // version at which the document transitioned to a FOUND_DOCUMENT.
- if (this.createTime.isEqual(SnapshotVersion.min()) &&
- (this.documentType === 2 /* DocumentType.NO_DOCUMENT */ ||
- this.documentType === 0 /* DocumentType.INVALID */)) {
- this.createTime = version;
- }
- this.version = version;
- this.documentType = 1 /* DocumentType.FOUND_DOCUMENT */;
- this.data = value;
- this.documentState = 0 /* DocumentState.SYNCED */;
- return this;
- }
- /**
- * Changes the document type to indicate that it doesn't exist at the given
- * version.
- */
- convertToNoDocument(version) {
- this.version = version;
- this.documentType = 2 /* DocumentType.NO_DOCUMENT */;
- this.data = ObjectValue.empty();
- this.documentState = 0 /* DocumentState.SYNCED */;
- return this;
- }
- /**
- * Changes the document type to indicate that it exists at a given version but
- * that its data is not known (e.g. a document that was updated without a known
- * base document).
- */
- convertToUnknownDocument(version) {
- this.version = version;
- this.documentType = 3 /* DocumentType.UNKNOWN_DOCUMENT */;
- this.data = ObjectValue.empty();
- this.documentState = 2 /* DocumentState.HAS_COMMITTED_MUTATIONS */;
- return this;
- }
- setHasCommittedMutations() {
- this.documentState = 2 /* DocumentState.HAS_COMMITTED_MUTATIONS */;
- return this;
- }
- setHasLocalMutations() {
- this.documentState = 1 /* DocumentState.HAS_LOCAL_MUTATIONS */;
- this.version = SnapshotVersion.min();
- return this;
- }
- setReadTime(readTime) {
- this.readTime = readTime;
- return this;
- }
- get hasLocalMutations() {
- return this.documentState === 1 /* DocumentState.HAS_LOCAL_MUTATIONS */;
- }
- get hasCommittedMutations() {
- return this.documentState === 2 /* DocumentState.HAS_COMMITTED_MUTATIONS */;
- }
- get hasPendingWrites() {
- return this.hasLocalMutations || this.hasCommittedMutations;
- }
- isValidDocument() {
- return this.documentType !== 0 /* DocumentType.INVALID */;
- }
- isFoundDocument() {
- return this.documentType === 1 /* DocumentType.FOUND_DOCUMENT */;
- }
- isNoDocument() {
- return this.documentType === 2 /* DocumentType.NO_DOCUMENT */;
- }
- isUnknownDocument() {
- return this.documentType === 3 /* DocumentType.UNKNOWN_DOCUMENT */;
- }
- isEqual(other) {
- return (other instanceof MutableDocument &&
- this.key.isEqual(other.key) &&
- this.version.isEqual(other.version) &&
- this.documentType === other.documentType &&
- this.documentState === other.documentState &&
- this.data.isEqual(other.data));
- }
- mutableCopy() {
- return new MutableDocument(this.key, this.documentType, this.version, this.readTime, this.createTime, this.data.clone(), this.documentState);
- }
- toString() {
- return (`Document(${this.key}, ${this.version}, ${JSON.stringify(this.data.value)}, ` +
- `{createTime: ${this.createTime}}), ` +
- `{documentType: ${this.documentType}}), ` +
- `{documentState: ${this.documentState}})`);
- }
- }
- /**
- * Compares the value for field `field` in the provided documents. Throws if
- * the field does not exist in both documents.
- */
- function compareDocumentsByField(field, d1, d2) {
- const v1 = d1.data.field(field);
- const v2 = d2.data.field(field);
- if (v1 !== null && v2 !== null) {
- return valueCompare(v1, v2);
- }
- else {
- return fail();
- }
- }
- /**
- * @license
- * Copyright 2022 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Represents a bound of a query.
- *
- * The bound is specified with the given components representing a position and
- * whether it's just before or just after the position (relative to whatever the
- * query order is).
- *
- * The position represents a logical index position for a query. It's a prefix
- * of values for the (potentially implicit) order by clauses of a query.
- *
- * Bound provides a function to determine whether a document comes before or
- * after a bound. This is influenced by whether the position is just before or
- * just after the provided values.
- */
- class Bound {
- constructor(position, inclusive) {
- this.position = position;
- this.inclusive = inclusive;
- }
- }
- function boundCompareToDocument(bound, orderBy, doc) {
- let comparison = 0;
- for (let i = 0; i < bound.position.length; i++) {
- const orderByComponent = orderBy[i];
- const component = bound.position[i];
- if (orderByComponent.field.isKeyField()) {
- comparison = DocumentKey.comparator(DocumentKey.fromName(component.referenceValue), doc.key);
- }
- else {
- const docValue = doc.data.field(orderByComponent.field);
- comparison = valueCompare(component, docValue);
- }
- if (orderByComponent.dir === "desc" /* Direction.DESCENDING */) {
- comparison = comparison * -1;
- }
- if (comparison !== 0) {
- break;
- }
- }
- return comparison;
- }
- /**
- * Returns true if a document sorts after a bound using the provided sort
- * order.
- */
- function boundSortsAfterDocument(bound, orderBy, doc) {
- const comparison = boundCompareToDocument(bound, orderBy, doc);
- return bound.inclusive ? comparison >= 0 : comparison > 0;
- }
- /**
- * Returns true if a document sorts before a bound using the provided sort
- * order.
- */
- function boundSortsBeforeDocument(bound, orderBy, doc) {
- const comparison = boundCompareToDocument(bound, orderBy, doc);
- return bound.inclusive ? comparison <= 0 : comparison < 0;
- }
- function boundEquals(left, right) {
- if (left === null) {
- return right === null;
- }
- else if (right === null) {
- return false;
- }
- if (left.inclusive !== right.inclusive ||
- left.position.length !== right.position.length) {
- return false;
- }
- for (let i = 0; i < left.position.length; i++) {
- const leftPosition = left.position[i];
- const rightPosition = right.position[i];
- if (!valueEquals(leftPosition, rightPosition)) {
- return false;
- }
- }
- return true;
- }
- /**
- * @license
- * Copyright 2022 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * An ordering on a field, in some Direction. Direction defaults to ASCENDING.
- */
- class OrderBy {
- constructor(field, dir = "asc" /* Direction.ASCENDING */) {
- this.field = field;
- this.dir = dir;
- }
- }
- function canonifyOrderBy(orderBy) {
- // TODO(b/29183165): Make this collision robust.
- return orderBy.field.canonicalString() + orderBy.dir;
- }
- function stringifyOrderBy(orderBy) {
- return `${orderBy.field.canonicalString()} (${orderBy.dir})`;
- }
- function orderByEquals(left, right) {
- return left.dir === right.dir && left.field.isEqual(right.field);
- }
- /**
- * @license
- * Copyright 2022 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- class Filter {
- }
- class FieldFilter extends Filter {
- constructor(field, op, value) {
- super();
- this.field = field;
- this.op = op;
- this.value = value;
- }
- /**
- * Creates a filter based on the provided arguments.
- */
- static create(field, op, value) {
- if (field.isKeyField()) {
- if (op === "in" /* Operator.IN */ || op === "not-in" /* Operator.NOT_IN */) {
- return this.createKeyFieldInFilter(field, op, value);
- }
- else {
- return new KeyFieldFilter(field, op, value);
- }
- }
- else if (op === "array-contains" /* Operator.ARRAY_CONTAINS */) {
- return new ArrayContainsFilter(field, value);
- }
- else if (op === "in" /* Operator.IN */) {
- return new InFilter(field, value);
- }
- else if (op === "not-in" /* Operator.NOT_IN */) {
- return new NotInFilter(field, value);
- }
- else if (op === "array-contains-any" /* Operator.ARRAY_CONTAINS_ANY */) {
- return new ArrayContainsAnyFilter(field, value);
- }
- else {
- return new FieldFilter(field, op, value);
- }
- }
- static createKeyFieldInFilter(field, op, value) {
- return op === "in" /* Operator.IN */
- ? new KeyFieldInFilter(field, value)
- : new KeyFieldNotInFilter(field, value);
- }
- matches(doc) {
- const other = doc.data.field(this.field);
- // Types do not have to match in NOT_EQUAL filters.
- if (this.op === "!=" /* Operator.NOT_EQUAL */) {
- return (other !== null &&
- this.matchesComparison(valueCompare(other, this.value)));
- }
- // Only compare types with matching backend order (such as double and int).
- return (other !== null &&
- typeOrder(this.value) === typeOrder(other) &&
- this.matchesComparison(valueCompare(other, this.value)));
- }
- matchesComparison(comparison) {
- switch (this.op) {
- case "<" /* Operator.LESS_THAN */:
- return comparison < 0;
- case "<=" /* Operator.LESS_THAN_OR_EQUAL */:
- return comparison <= 0;
- case "==" /* Operator.EQUAL */:
- return comparison === 0;
- case "!=" /* Operator.NOT_EQUAL */:
- return comparison !== 0;
- case ">" /* Operator.GREATER_THAN */:
- return comparison > 0;
- case ">=" /* Operator.GREATER_THAN_OR_EQUAL */:
- return comparison >= 0;
- default:
- return fail();
- }
- }
- isInequality() {
- return ([
- "<" /* Operator.LESS_THAN */,
- "<=" /* Operator.LESS_THAN_OR_EQUAL */,
- ">" /* Operator.GREATER_THAN */,
- ">=" /* Operator.GREATER_THAN_OR_EQUAL */,
- "!=" /* Operator.NOT_EQUAL */,
- "not-in" /* Operator.NOT_IN */
- ].indexOf(this.op) >= 0);
- }
- getFlattenedFilters() {
- return [this];
- }
- getFilters() {
- return [this];
- }
- getFirstInequalityField() {
- if (this.isInequality()) {
- return this.field;
- }
- return null;
- }
- }
- class CompositeFilter extends Filter {
- constructor(filters, op) {
- super();
- this.filters = filters;
- this.op = op;
- this.memoizedFlattenedFilters = null;
- }
- /**
- * Creates a filter based on the provided arguments.
- */
- static create(filters, op) {
- return new CompositeFilter(filters, op);
- }
- matches(doc) {
- if (compositeFilterIsConjunction(this)) {
- // For conjunctions, all filters must match, so return false if any filter doesn't match.
- return this.filters.find(filter => !filter.matches(doc)) === undefined;
- }
- else {
- // For disjunctions, at least one filter should match.
- return this.filters.find(filter => filter.matches(doc)) !== undefined;
- }
- }
- getFlattenedFilters() {
- if (this.memoizedFlattenedFilters !== null) {
- return this.memoizedFlattenedFilters;
- }
- this.memoizedFlattenedFilters = this.filters.reduce((result, subfilter) => {
- return result.concat(subfilter.getFlattenedFilters());
- }, []);
- return this.memoizedFlattenedFilters;
- }
- // Returns a mutable copy of `this.filters`
- getFilters() {
- return Object.assign([], this.filters);
- }
- getFirstInequalityField() {
- const found = this.findFirstMatchingFilter(filter => filter.isInequality());
- if (found !== null) {
- return found.field;
- }
- return null;
- }
- // Performs a depth-first search to find and return the first FieldFilter in the composite filter
- // that satisfies the predicate. Returns `null` if none of the FieldFilters satisfy the
- // predicate.
- findFirstMatchingFilter(predicate) {
- for (const fieldFilter of this.getFlattenedFilters()) {
- if (predicate(fieldFilter)) {
- return fieldFilter;
- }
- }
- return null;
- }
- }
- function compositeFilterIsConjunction(compositeFilter) {
- return compositeFilter.op === "and" /* CompositeOperator.AND */;
- }
- function compositeFilterIsDisjunction(compositeFilter) {
- return compositeFilter.op === "or" /* CompositeOperator.OR */;
- }
- /**
- * Returns true if this filter is a conjunction of field filters only. Returns false otherwise.
- */
- function compositeFilterIsFlatConjunction(compositeFilter) {
- return (compositeFilterIsFlat(compositeFilter) &&
- compositeFilterIsConjunction(compositeFilter));
- }
- /**
- * Returns true if this filter does not contain any composite filters. Returns false otherwise.
- */
- function compositeFilterIsFlat(compositeFilter) {
- for (const filter of compositeFilter.filters) {
- if (filter instanceof CompositeFilter) {
- return false;
- }
- }
- return true;
- }
- function canonifyFilter(filter) {
- if (filter instanceof FieldFilter) {
- // TODO(b/29183165): Technically, this won't be unique if two values have
- // the same description, such as the int 3 and the string "3". So we should
- // add the types in here somehow, too.
- return (filter.field.canonicalString() +
- filter.op.toString() +
- canonicalId(filter.value));
- }
- else if (compositeFilterIsFlatConjunction(filter)) {
- // Older SDK versions use an implicit AND operation between their filters.
- // In the new SDK versions, the developer may use an explicit AND filter.
- // To stay consistent with the old usages, we add a special case to ensure
- // the canonical ID for these two are the same. For example:
- // `col.whereEquals("a", 1).whereEquals("b", 2)` should have the same
- // canonical ID as `col.where(and(equals("a",1), equals("b",2)))`.
- return filter.filters.map(filter => canonifyFilter(filter)).join(',');
- }
- else {
- // filter instanceof CompositeFilter
- const canonicalIdsString = filter.filters
- .map(filter => canonifyFilter(filter))
- .join(',');
- return `${filter.op}(${canonicalIdsString})`;
- }
- }
- function filterEquals(f1, f2) {
- if (f1 instanceof FieldFilter) {
- return fieldFilterEquals(f1, f2);
- }
- else if (f1 instanceof CompositeFilter) {
- return compositeFilterEquals(f1, f2);
- }
- else {
- fail();
- }
- }
- function fieldFilterEquals(f1, f2) {
- return (f2 instanceof FieldFilter &&
- f1.op === f2.op &&
- f1.field.isEqual(f2.field) &&
- valueEquals(f1.value, f2.value));
- }
- function compositeFilterEquals(f1, f2) {
- if (f2 instanceof CompositeFilter &&
- f1.op === f2.op &&
- f1.filters.length === f2.filters.length) {
- const subFiltersMatch = f1.filters.reduce((result, f1Filter, index) => result && filterEquals(f1Filter, f2.filters[index]), true);
- return subFiltersMatch;
- }
- return false;
- }
- /**
- * Returns a new composite filter that contains all filter from
- * `compositeFilter` plus all the given filters in `otherFilters`.
- */
- function compositeFilterWithAddedFilters(compositeFilter, otherFilters) {
- const mergedFilters = compositeFilter.filters.concat(otherFilters);
- return CompositeFilter.create(mergedFilters, compositeFilter.op);
- }
- /** Returns a debug description for `filter`. */
- function stringifyFilter(filter) {
- if (filter instanceof FieldFilter) {
- return stringifyFieldFilter(filter);
- }
- else if (filter instanceof CompositeFilter) {
- return stringifyCompositeFilter(filter);
- }
- else {
- return 'Filter';
- }
- }
- function stringifyCompositeFilter(filter) {
- return (filter.op.toString() +
- ` {` +
- filter.getFilters().map(stringifyFilter).join(' ,') +
- '}');
- }
- function stringifyFieldFilter(filter) {
- return `${filter.field.canonicalString()} ${filter.op} ${canonicalId(filter.value)}`;
- }
- /** Filter that matches on key fields (i.e. '__name__'). */
- class KeyFieldFilter extends FieldFilter {
- constructor(field, op, value) {
- super(field, op, value);
- this.key = DocumentKey.fromName(value.referenceValue);
- }
- matches(doc) {
- const comparison = DocumentKey.comparator(doc.key, this.key);
- return this.matchesComparison(comparison);
- }
- }
- /** Filter that matches on key fields within an array. */
- class KeyFieldInFilter extends FieldFilter {
- constructor(field, value) {
- super(field, "in" /* Operator.IN */, value);
- this.keys = extractDocumentKeysFromArrayValue("in" /* Operator.IN */, value);
- }
- matches(doc) {
- return this.keys.some(key => key.isEqual(doc.key));
- }
- }
- /** Filter that matches on key fields not present within an array. */
- class KeyFieldNotInFilter extends FieldFilter {
- constructor(field, value) {
- super(field, "not-in" /* Operator.NOT_IN */, value);
- this.keys = extractDocumentKeysFromArrayValue("not-in" /* Operator.NOT_IN */, value);
- }
- matches(doc) {
- return !this.keys.some(key => key.isEqual(doc.key));
- }
- }
- function extractDocumentKeysFromArrayValue(op, value) {
- var _a;
- return (((_a = value.arrayValue) === null || _a === void 0 ? void 0 : _a.values) || []).map(v => {
- return DocumentKey.fromName(v.referenceValue);
- });
- }
- /** A Filter that implements the array-contains operator. */
- class ArrayContainsFilter extends FieldFilter {
- constructor(field, value) {
- super(field, "array-contains" /* Operator.ARRAY_CONTAINS */, value);
- }
- matches(doc) {
- const other = doc.data.field(this.field);
- return isArray(other) && arrayValueContains(other.arrayValue, this.value);
- }
- }
- /** A Filter that implements the IN operator. */
- class InFilter extends FieldFilter {
- constructor(field, value) {
- super(field, "in" /* Operator.IN */, value);
- }
- matches(doc) {
- const other = doc.data.field(this.field);
- return other !== null && arrayValueContains(this.value.arrayValue, other);
- }
- }
- /** A Filter that implements the not-in operator. */
- class NotInFilter extends FieldFilter {
- constructor(field, value) {
- super(field, "not-in" /* Operator.NOT_IN */, value);
- }
- matches(doc) {
- if (arrayValueContains(this.value.arrayValue, { nullValue: 'NULL_VALUE' })) {
- return false;
- }
- const other = doc.data.field(this.field);
- return other !== null && !arrayValueContains(this.value.arrayValue, other);
- }
- }
- /** A Filter that implements the array-contains-any operator. */
- class ArrayContainsAnyFilter extends FieldFilter {
- constructor(field, value) {
- super(field, "array-contains-any" /* Operator.ARRAY_CONTAINS_ANY */, value);
- }
- matches(doc) {
- const other = doc.data.field(this.field);
- if (!isArray(other) || !other.arrayValue.values) {
- return false;
- }
- return other.arrayValue.values.some(val => arrayValueContains(this.value.arrayValue, val));
- }
- }
- /**
- * @license
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- // Visible for testing
- class TargetImpl {
- constructor(path, collectionGroup = null, orderBy = [], filters = [], limit = null, startAt = null, endAt = null) {
- this.path = path;
- this.collectionGroup = collectionGroup;
- this.orderBy = orderBy;
- this.filters = filters;
- this.limit = limit;
- this.startAt = startAt;
- this.endAt = endAt;
- this.memoizedCanonicalId = null;
- }
- }
- /**
- * Initializes a Target with a path and optional additional query constraints.
- * Path must currently be empty if this is a collection group query.
- *
- * NOTE: you should always construct `Target` from `Query.toTarget` instead of
- * using this factory method, because `Query` provides an implicit `orderBy`
- * property.
- */
- function newTarget(path, collectionGroup = null, orderBy = [], filters = [], limit = null, startAt = null, endAt = null) {
- return new TargetImpl(path, collectionGroup, orderBy, filters, limit, startAt, endAt);
- }
- function canonifyTarget(target) {
- const targetImpl = debugCast(target);
- if (targetImpl.memoizedCanonicalId === null) {
- let str = targetImpl.path.canonicalString();
- if (targetImpl.collectionGroup !== null) {
- str += '|cg:' + targetImpl.collectionGroup;
- }
- str += '|f:';
- str += targetImpl.filters.map(f => canonifyFilter(f)).join(',');
- str += '|ob:';
- str += targetImpl.orderBy.map(o => canonifyOrderBy(o)).join(',');
- if (!isNullOrUndefined(targetImpl.limit)) {
- str += '|l:';
- str += targetImpl.limit;
- }
- if (targetImpl.startAt) {
- str += '|lb:';
- str += targetImpl.startAt.inclusive ? 'b:' : 'a:';
- str += targetImpl.startAt.position.map(p => canonicalId(p)).join(',');
- }
- if (targetImpl.endAt) {
- str += '|ub:';
- str += targetImpl.endAt.inclusive ? 'a:' : 'b:';
- str += targetImpl.endAt.position.map(p => canonicalId(p)).join(',');
- }
- targetImpl.memoizedCanonicalId = str;
- }
- return targetImpl.memoizedCanonicalId;
- }
- function stringifyTarget(target) {
- let str = target.path.canonicalString();
- if (target.collectionGroup !== null) {
- str += ' collectionGroup=' + target.collectionGroup;
- }
- if (target.filters.length > 0) {
- str += `, filters: [${target.filters
- .map(f => stringifyFilter(f))
- .join(', ')}]`;
- }
- if (!isNullOrUndefined(target.limit)) {
- str += ', limit: ' + target.limit;
- }
- if (target.orderBy.length > 0) {
- str += `, orderBy: [${target.orderBy
- .map(o => stringifyOrderBy(o))
- .join(', ')}]`;
- }
- if (target.startAt) {
- str += ', startAt: ';
- str += target.startAt.inclusive ? 'b:' : 'a:';
- str += target.startAt.position.map(p => canonicalId(p)).join(',');
- }
- if (target.endAt) {
- str += ', endAt: ';
- str += target.endAt.inclusive ? 'a:' : 'b:';
- str += target.endAt.position.map(p => canonicalId(p)).join(',');
- }
- return `Target(${str})`;
- }
- function targetEquals(left, right) {
- if (left.limit !== right.limit) {
- return false;
- }
- if (left.orderBy.length !== right.orderBy.length) {
- return false;
- }
- for (let i = 0; i < left.orderBy.length; i++) {
- if (!orderByEquals(left.orderBy[i], right.orderBy[i])) {
- return false;
- }
- }
- if (left.filters.length !== right.filters.length) {
- return false;
- }
- for (let i = 0; i < left.filters.length; i++) {
- if (!filterEquals(left.filters[i], right.filters[i])) {
- return false;
- }
- }
- if (left.collectionGroup !== right.collectionGroup) {
- return false;
- }
- if (!left.path.isEqual(right.path)) {
- return false;
- }
- if (!boundEquals(left.startAt, right.startAt)) {
- return false;
- }
- return boundEquals(left.endAt, right.endAt);
- }
- function targetIsDocumentTarget(target) {
- return (DocumentKey.isDocumentKey(target.path) &&
- target.collectionGroup === null &&
- target.filters.length === 0);
- }
- /** Returns the field filters that target the given field path. */
- function targetGetFieldFiltersForPath(target, path) {
- return target.filters.filter(f => f instanceof FieldFilter && f.field.isEqual(path));
- }
- /**
- * Returns the values that are used in ARRAY_CONTAINS or ARRAY_CONTAINS_ANY
- * filters. Returns `null` if there are no such filters.
- */
- function targetGetArrayValues(target, fieldIndex) {
- const segment = fieldIndexGetArraySegment(fieldIndex);
- if (segment === undefined) {
- return null;
- }
- for (const fieldFilter of targetGetFieldFiltersForPath(target, segment.fieldPath)) {
- switch (fieldFilter.op) {
- case "array-contains-any" /* Operator.ARRAY_CONTAINS_ANY */:
- return fieldFilter.value.arrayValue.values || [];
- case "array-contains" /* Operator.ARRAY_CONTAINS */:
- return [fieldFilter.value];
- // Remaining filters are not array filters.
- }
- }
- return null;
- }
- /**
- * Returns the list of values that are used in != or NOT_IN filters. Returns
- * `null` if there are no such filters.
- */
- function targetGetNotInValues(target, fieldIndex) {
- const values = new Map();
- for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
- for (const fieldFilter of targetGetFieldFiltersForPath(target, segment.fieldPath)) {
- switch (fieldFilter.op) {
- case "==" /* Operator.EQUAL */:
- case "in" /* Operator.IN */:
- // Encode equality prefix, which is encoded in the index value before
- // the inequality (e.g. `a == 'a' && b != 'b'` is encoded to
- // `value != 'ab'`).
- values.set(segment.fieldPath.canonicalString(), fieldFilter.value);
- break;
- case "not-in" /* Operator.NOT_IN */:
- case "!=" /* Operator.NOT_EQUAL */:
- // NotIn/NotEqual is always a suffix. There cannot be any remaining
- // segments and hence we can return early here.
- values.set(segment.fieldPath.canonicalString(), fieldFilter.value);
- return Array.from(values.values());
- // Remaining filters cannot be used as notIn bounds.
- }
- }
- }
- return null;
- }
- /**
- * Returns a lower bound of field values that can be used as a starting point to
- * scan the index defined by `fieldIndex`. Returns `MIN_VALUE` if no lower bound
- * exists.
- */
- function targetGetLowerBound(target, fieldIndex) {
- const values = [];
- let inclusive = true;
- // For each segment, retrieve a lower bound if there is a suitable filter or
- // startAt.
- for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
- const segmentBound = segment.kind === 0 /* IndexKind.ASCENDING */
- ? targetGetAscendingBound(target, segment.fieldPath, target.startAt)
- : targetGetDescendingBound(target, segment.fieldPath, target.startAt);
- values.push(segmentBound.value);
- inclusive && (inclusive = segmentBound.inclusive);
- }
- return new Bound(values, inclusive);
- }
- /**
- * Returns an upper bound of field values that can be used as an ending point
- * when scanning the index defined by `fieldIndex`. Returns `MAX_VALUE` if no
- * upper bound exists.
- */
- function targetGetUpperBound(target, fieldIndex) {
- const values = [];
- let inclusive = true;
- // For each segment, retrieve an upper bound if there is a suitable filter or
- // endAt.
- for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
- const segmentBound = segment.kind === 0 /* IndexKind.ASCENDING */
- ? targetGetDescendingBound(target, segment.fieldPath, target.endAt)
- : targetGetAscendingBound(target, segment.fieldPath, target.endAt);
- values.push(segmentBound.value);
- inclusive && (inclusive = segmentBound.inclusive);
- }
- return new Bound(values, inclusive);
- }
- /**
- * Returns the value to use as the lower bound for ascending index segment at
- * the provided `fieldPath` (or the upper bound for an descending segment).
- */
- function targetGetAscendingBound(target, fieldPath, bound) {
- let value = MIN_VALUE;
- let inclusive = true;
- // Process all filters to find a value for the current field segment
- for (const fieldFilter of targetGetFieldFiltersForPath(target, fieldPath)) {
- let filterValue = MIN_VALUE;
- let filterInclusive = true;
- switch (fieldFilter.op) {
- case "<" /* Operator.LESS_THAN */:
- case "<=" /* Operator.LESS_THAN_OR_EQUAL */:
- filterValue = valuesGetLowerBound(fieldFilter.value);
- break;
- case "==" /* Operator.EQUAL */:
- case "in" /* Operator.IN */:
- case ">=" /* Operator.GREATER_THAN_OR_EQUAL */:
- filterValue = fieldFilter.value;
- break;
- case ">" /* Operator.GREATER_THAN */:
- filterValue = fieldFilter.value;
- filterInclusive = false;
- break;
- case "!=" /* Operator.NOT_EQUAL */:
- case "not-in" /* Operator.NOT_IN */:
- filterValue = MIN_VALUE;
- break;
- // Remaining filters cannot be used as lower bounds.
- }
- if (lowerBoundCompare({ value, inclusive }, { value: filterValue, inclusive: filterInclusive }) < 0) {
- value = filterValue;
- inclusive = filterInclusive;
- }
- }
- // If there is an additional bound, compare the values against the existing
- // range to see if we can narrow the scope.
- if (bound !== null) {
- for (let i = 0; i < target.orderBy.length; ++i) {
- const orderBy = target.orderBy[i];
- if (orderBy.field.isEqual(fieldPath)) {
- const cursorValue = bound.position[i];
- if (lowerBoundCompare({ value, inclusive }, { value: cursorValue, inclusive: bound.inclusive }) < 0) {
- value = cursorValue;
- inclusive = bound.inclusive;
- }
- break;
- }
- }
- }
- return { value, inclusive };
- }
- /**
- * Returns the value to use as the upper bound for ascending index segment at
- * the provided `fieldPath` (or the lower bound for a descending segment).
- */
- function targetGetDescendingBound(target, fieldPath, bound) {
- let value = MAX_VALUE;
- let inclusive = true;
- // Process all filters to find a value for the current field segment
- for (const fieldFilter of targetGetFieldFiltersForPath(target, fieldPath)) {
- let filterValue = MAX_VALUE;
- let filterInclusive = true;
- switch (fieldFilter.op) {
- case ">=" /* Operator.GREATER_THAN_OR_EQUAL */:
- case ">" /* Operator.GREATER_THAN */:
- filterValue = valuesGetUpperBound(fieldFilter.value);
- filterInclusive = false;
- break;
- case "==" /* Operator.EQUAL */:
- case "in" /* Operator.IN */:
- case "<=" /* Operator.LESS_THAN_OR_EQUAL */:
- filterValue = fieldFilter.value;
- break;
- case "<" /* Operator.LESS_THAN */:
- filterValue = fieldFilter.value;
- filterInclusive = false;
- break;
- case "!=" /* Operator.NOT_EQUAL */:
- case "not-in" /* Operator.NOT_IN */:
- filterValue = MAX_VALUE;
- break;
- // Remaining filters cannot be used as upper bounds.
- }
- if (upperBoundCompare({ value, inclusive }, { value: filterValue, inclusive: filterInclusive }) > 0) {
- value = filterValue;
- inclusive = filterInclusive;
- }
- }
- // If there is an additional bound, compare the values against the existing
- // range to see if we can narrow the scope.
- if (bound !== null) {
- for (let i = 0; i < target.orderBy.length; ++i) {
- const orderBy = target.orderBy[i];
- if (orderBy.field.isEqual(fieldPath)) {
- const cursorValue = bound.position[i];
- if (upperBoundCompare({ value, inclusive }, { value: cursorValue, inclusive: bound.inclusive }) > 0) {
- value = cursorValue;
- inclusive = bound.inclusive;
- }
- break;
- }
- }
- }
- return { value, inclusive };
- }
- /** Returns the number of segments of a perfect index for this target. */
- function targetGetSegmentCount(target) {
- let fields = new SortedSet(FieldPath$1.comparator);
- let hasArraySegment = false;
- for (const filter of target.filters) {
- for (const subFilter of filter.getFlattenedFilters()) {
- // __name__ is not an explicit segment of any index, so we don't need to
- // count it.
- if (subFilter.field.isKeyField()) {
- continue;
- }
- // ARRAY_CONTAINS or ARRAY_CONTAINS_ANY filters must be counted separately.
- // For instance, it is possible to have an index for "a ARRAY a ASC". Even
- // though these are on the same field, they should be counted as two
- // separate segments in an index.
- if (subFilter.op === "array-contains" /* Operator.ARRAY_CONTAINS */ ||
- subFilter.op === "array-contains-any" /* Operator.ARRAY_CONTAINS_ANY */) {
- hasArraySegment = true;
- }
- else {
- fields = fields.add(subFilter.field);
- }
- }
- }
- for (const orderBy of target.orderBy) {
- // __name__ is not an explicit segment of any index, so we don't need to
- // count it.
- if (!orderBy.field.isKeyField()) {
- fields = fields.add(orderBy.field);
- }
- }
- return fields.size + (hasArraySegment ? 1 : 0);
- }
- function targetHasLimit(target) {
- return target.limit !== null;
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Query encapsulates all the query attributes we support in the SDK. It can
- * be run against the LocalStore, as well as be converted to a `Target` to
- * query the RemoteStore results.
- *
- * Visible for testing.
- */
- class QueryImpl {
- /**
- * Initializes a Query with a path and optional additional query constraints.
- * Path must currently be empty if this is a collection group query.
- */
- constructor(path, collectionGroup = null, explicitOrderBy = [], filters = [], limit = null, limitType = "F" /* LimitType.First */, startAt = null, endAt = null) {
- this.path = path;
- this.collectionGroup = collectionGroup;
- this.explicitOrderBy = explicitOrderBy;
- this.filters = filters;
- this.limit = limit;
- this.limitType = limitType;
- this.startAt = startAt;
- this.endAt = endAt;
- this.memoizedOrderBy = null;
- // The corresponding `Target` of this `Query` instance.
- this.memoizedTarget = null;
- if (this.startAt) ;
- if (this.endAt) ;
- }
- }
- /** Creates a new Query instance with the options provided. */
- function newQuery(path, collectionGroup, explicitOrderBy, filters, limit, limitType, startAt, endAt) {
- return new QueryImpl(path, collectionGroup, explicitOrderBy, filters, limit, limitType, startAt, endAt);
- }
- /** Creates a new Query for a query that matches all documents at `path` */
- function newQueryForPath(path) {
- return new QueryImpl(path);
- }
- /**
- * Helper to convert a collection group query into a collection query at a
- * specific path. This is used when executing collection group queries, since
- * we have to split the query into a set of collection queries at multiple
- * paths.
- */
- function asCollectionQueryAtPath(query, path) {
- return new QueryImpl(path,
- /*collectionGroup=*/ null, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, query.startAt, query.endAt);
- }
- /**
- * Returns true if this query does not specify any query constraints that
- * could remove results.
- */
- function queryMatchesAllDocuments(query) {
- return (query.filters.length === 0 &&
- query.limit === null &&
- query.startAt == null &&
- query.endAt == null &&
- (query.explicitOrderBy.length === 0 ||
- (query.explicitOrderBy.length === 1 &&
- query.explicitOrderBy[0].field.isKeyField())));
- }
- function getFirstOrderByField(query) {
- return query.explicitOrderBy.length > 0
- ? query.explicitOrderBy[0].field
- : null;
- }
- function getInequalityFilterField(query) {
- for (const filter of query.filters) {
- const result = filter.getFirstInequalityField();
- if (result !== null) {
- return result;
- }
- }
- return null;
- }
- /**
- * Creates a new Query for a collection group query that matches all documents
- * within the provided collection group.
- */
- function newQueryForCollectionGroup(collectionId) {
- return new QueryImpl(ResourcePath.emptyPath(), collectionId);
- }
- /**
- * Returns whether the query matches a single document by path (rather than a
- * collection).
- */
- function isDocumentQuery$1(query) {
- return (DocumentKey.isDocumentKey(query.path) &&
- query.collectionGroup === null &&
- query.filters.length === 0);
- }
- /**
- * Returns whether the query matches a collection group rather than a specific
- * collection.
- */
- function isCollectionGroupQuery(query) {
- return query.collectionGroup !== null;
- }
- /**
- * Returns the implicit order by constraint that is used to execute the Query,
- * which can be different from the order by constraints the user provided (e.g.
- * the SDK and backend always orders by `__name__`).
- */
- function queryOrderBy(query) {
- const queryImpl = debugCast(query);
- if (queryImpl.memoizedOrderBy === null) {
- queryImpl.memoizedOrderBy = [];
- const inequalityField = getInequalityFilterField(queryImpl);
- const firstOrderByField = getFirstOrderByField(queryImpl);
- if (inequalityField !== null && firstOrderByField === null) {
- // In order to implicitly add key ordering, we must also add the
- // inequality filter field for it to be a valid query.
- // Note that the default inequality field and key ordering is ascending.
- if (!inequalityField.isKeyField()) {
- queryImpl.memoizedOrderBy.push(new OrderBy(inequalityField));
- }
- queryImpl.memoizedOrderBy.push(new OrderBy(FieldPath$1.keyField(), "asc" /* Direction.ASCENDING */));
- }
- else {
- let foundKeyOrdering = false;
- for (const orderBy of queryImpl.explicitOrderBy) {
- queryImpl.memoizedOrderBy.push(orderBy);
- if (orderBy.field.isKeyField()) {
- foundKeyOrdering = true;
- }
- }
- if (!foundKeyOrdering) {
- // The order of the implicit key ordering always matches the last
- // explicit order by
- const lastDirection = queryImpl.explicitOrderBy.length > 0
- ? queryImpl.explicitOrderBy[queryImpl.explicitOrderBy.length - 1]
- .dir
- : "asc" /* Direction.ASCENDING */;
- queryImpl.memoizedOrderBy.push(new OrderBy(FieldPath$1.keyField(), lastDirection));
- }
- }
- }
- return queryImpl.memoizedOrderBy;
- }
- /**
- * Converts this `Query` instance to it's corresponding `Target` representation.
- */
- function queryToTarget(query) {
- const queryImpl = debugCast(query);
- if (!queryImpl.memoizedTarget) {
- if (queryImpl.limitType === "F" /* LimitType.First */) {
- queryImpl.memoizedTarget = newTarget(queryImpl.path, queryImpl.collectionGroup, queryOrderBy(queryImpl), queryImpl.filters, queryImpl.limit, queryImpl.startAt, queryImpl.endAt);
- }
- else {
- // Flip the orderBy directions since we want the last results
- const orderBys = [];
- for (const orderBy of queryOrderBy(queryImpl)) {
- const dir = orderBy.dir === "desc" /* Direction.DESCENDING */
- ? "asc" /* Direction.ASCENDING */
- : "desc" /* Direction.DESCENDING */;
- orderBys.push(new OrderBy(orderBy.field, dir));
- }
- // We need to swap the cursors to match the now-flipped query ordering.
- const startAt = queryImpl.endAt
- ? new Bound(queryImpl.endAt.position, queryImpl.endAt.inclusive)
- : null;
- const endAt = queryImpl.startAt
- ? new Bound(queryImpl.startAt.position, queryImpl.startAt.inclusive)
- : null;
- // Now return as a LimitType.First query.
- queryImpl.memoizedTarget = newTarget(queryImpl.path, queryImpl.collectionGroup, orderBys, queryImpl.filters, queryImpl.limit, startAt, endAt);
- }
- }
- return queryImpl.memoizedTarget;
- }
- function queryWithAddedFilter(query, filter) {
- filter.getFirstInequalityField();
- getInequalityFilterField(query);
- const newFilters = query.filters.concat([filter]);
- return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), newFilters, query.limit, query.limitType, query.startAt, query.endAt);
- }
- function queryWithAddedOrderBy(query, orderBy) {
- // TODO(dimond): validate that orderBy does not list the same key twice.
- const newOrderBy = query.explicitOrderBy.concat([orderBy]);
- return new QueryImpl(query.path, query.collectionGroup, newOrderBy, query.filters.slice(), query.limit, query.limitType, query.startAt, query.endAt);
- }
- function queryWithLimit(query, limit, limitType) {
- return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), limit, limitType, query.startAt, query.endAt);
- }
- function queryWithStartAt(query, bound) {
- return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, bound, query.endAt);
- }
- function queryWithEndAt(query, bound) {
- return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, query.startAt, bound);
- }
- function queryEquals(left, right) {
- return (targetEquals(queryToTarget(left), queryToTarget(right)) &&
- left.limitType === right.limitType);
- }
- // TODO(b/29183165): This is used to get a unique string from a query to, for
- // example, use as a dictionary key, but the implementation is subject to
- // collisions. Make it collision-free.
- function canonifyQuery(query) {
- return `${canonifyTarget(queryToTarget(query))}|lt:${query.limitType}`;
- }
- function stringifyQuery(query) {
- return `Query(target=${stringifyTarget(queryToTarget(query))}; limitType=${query.limitType})`;
- }
- /** Returns whether `doc` matches the constraints of `query`. */
- function queryMatches(query, doc) {
- return (doc.isFoundDocument() &&
- queryMatchesPathAndCollectionGroup(query, doc) &&
- queryMatchesOrderBy(query, doc) &&
- queryMatchesFilters(query, doc) &&
- queryMatchesBounds(query, doc));
- }
- function queryMatchesPathAndCollectionGroup(query, doc) {
- const docPath = doc.key.path;
- if (query.collectionGroup !== null) {
- // NOTE: this.path is currently always empty since we don't expose Collection
- // Group queries rooted at a document path yet.
- return (doc.key.hasCollectionId(query.collectionGroup) &&
- query.path.isPrefixOf(docPath));
- }
- else if (DocumentKey.isDocumentKey(query.path)) {
- // exact match for document queries
- return query.path.isEqual(docPath);
- }
- else {
- // shallow ancestor queries by default
- return query.path.isImmediateParentOf(docPath);
- }
- }
- /**
- * A document must have a value for every ordering clause in order to show up
- * in the results.
- */
- function queryMatchesOrderBy(query, doc) {
- // We must use `queryOrderBy()` to get the list of all orderBys (both implicit and explicit).
- // Note that for OR queries, orderBy applies to all disjunction terms and implicit orderBys must
- // be taken into account. For example, the query "a > 1 || b==1" has an implicit "orderBy a" due
- // to the inequality, and is evaluated as "a > 1 orderBy a || b==1 orderBy a".
- // A document with content of {b:1} matches the filters, but does not match the orderBy because
- // it's missing the field 'a'.
- for (const orderBy of queryOrderBy(query)) {
- // order by key always matches
- if (!orderBy.field.isKeyField() && doc.data.field(orderBy.field) === null) {
- return false;
- }
- }
- return true;
- }
- function queryMatchesFilters(query, doc) {
- for (const filter of query.filters) {
- if (!filter.matches(doc)) {
- return false;
- }
- }
- return true;
- }
- /** Makes sure a document is within the bounds, if provided. */
- function queryMatchesBounds(query, doc) {
- if (query.startAt &&
- !boundSortsBeforeDocument(query.startAt, queryOrderBy(query), doc)) {
- return false;
- }
- if (query.endAt &&
- !boundSortsAfterDocument(query.endAt, queryOrderBy(query), doc)) {
- return false;
- }
- return true;
- }
- /**
- * Returns the collection group that this query targets.
- *
- * PORTING NOTE: This is only used in the Web SDK to facilitate multi-tab
- * synchronization for query results.
- */
- function queryCollectionGroup(query) {
- return (query.collectionGroup ||
- (query.path.length % 2 === 1
- ? query.path.lastSegment()
- : query.path.get(query.path.length - 2)));
- }
- /**
- * Returns a new comparator function that can be used to compare two documents
- * based on the Query's ordering constraint.
- */
- function newQueryComparator(query) {
- return (d1, d2) => {
- let comparedOnKeyField = false;
- for (const orderBy of queryOrderBy(query)) {
- const comp = compareDocs(orderBy, d1, d2);
- if (comp !== 0) {
- return comp;
- }
- comparedOnKeyField = comparedOnKeyField || orderBy.field.isKeyField();
- }
- return 0;
- };
- }
- function compareDocs(orderBy, d1, d2) {
- const comparison = orderBy.field.isKeyField()
- ? DocumentKey.comparator(d1.key, d2.key)
- : compareDocumentsByField(orderBy.field, d1, d2);
- switch (orderBy.dir) {
- case "asc" /* Direction.ASCENDING */:
- return comparison;
- case "desc" /* Direction.DESCENDING */:
- return -1 * comparison;
- default:
- return fail();
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * A map implementation that uses objects as keys. Objects must have an
- * associated equals function and must be immutable. Entries in the map are
- * stored together with the key being produced from the mapKeyFn. This map
- * automatically handles collisions of keys.
- */
- class ObjectMap {
- constructor(mapKeyFn, equalsFn) {
- this.mapKeyFn = mapKeyFn;
- this.equalsFn = equalsFn;
- /**
- * The inner map for a key/value pair. Due to the possibility of collisions we
- * keep a list of entries that we do a linear search through to find an actual
- * match. Note that collisions should be rare, so we still expect near
- * constant time lookups in practice.
- */
- this.inner = {};
- /** The number of entries stored in the map */
- this.innerSize = 0;
- }
- /** Get a value for this key, or undefined if it does not exist. */
- get(key) {
- const id = this.mapKeyFn(key);
- const matches = this.inner[id];
- if (matches === undefined) {
- return undefined;
- }
- for (const [otherKey, value] of matches) {
- if (this.equalsFn(otherKey, key)) {
- return value;
- }
- }
- return undefined;
- }
- has(key) {
- return this.get(key) !== undefined;
- }
- /** Put this key and value in the map. */
- set(key, value) {
- const id = this.mapKeyFn(key);
- const matches = this.inner[id];
- if (matches === undefined) {
- this.inner[id] = [[key, value]];
- this.innerSize++;
- return;
- }
- for (let i = 0; i < matches.length; i++) {
- if (this.equalsFn(matches[i][0], key)) {
- // This is updating an existing entry and does not increase `innerSize`.
- matches[i] = [key, value];
- return;
- }
- }
- matches.push([key, value]);
- this.innerSize++;
- }
- /**
- * Remove this key from the map. Returns a boolean if anything was deleted.
- */
- delete(key) {
- const id = this.mapKeyFn(key);
- const matches = this.inner[id];
- if (matches === undefined) {
- return false;
- }
- for (let i = 0; i < matches.length; i++) {
- if (this.equalsFn(matches[i][0], key)) {
- if (matches.length === 1) {
- delete this.inner[id];
- }
- else {
- matches.splice(i, 1);
- }
- this.innerSize--;
- return true;
- }
- }
- return false;
- }
- forEach(fn) {
- forEach(this.inner, (_, entries) => {
- for (const [k, v] of entries) {
- fn(k, v);
- }
- });
- }
- isEmpty() {
- return isEmpty(this.inner);
- }
- size() {
- return this.innerSize;
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const EMPTY_MUTABLE_DOCUMENT_MAP = new SortedMap(DocumentKey.comparator);
- function mutableDocumentMap() {
- return EMPTY_MUTABLE_DOCUMENT_MAP;
- }
- const EMPTY_DOCUMENT_MAP = new SortedMap(DocumentKey.comparator);
- function documentMap(...docs) {
- let map = EMPTY_DOCUMENT_MAP;
- for (const doc of docs) {
- map = map.insert(doc.key, doc);
- }
- return map;
- }
- function newOverlayedDocumentMap() {
- return newDocumentKeyMap();
- }
- function convertOverlayedDocumentMapToDocumentMap(collection) {
- let documents = EMPTY_DOCUMENT_MAP;
- collection.forEach((k, v) => (documents = documents.insert(k, v.overlayedDocument)));
- return documents;
- }
- function newOverlayMap() {
- return newDocumentKeyMap();
- }
- function newMutationMap() {
- return newDocumentKeyMap();
- }
- function newDocumentKeyMap() {
- return new ObjectMap(key => key.toString(), (l, r) => l.isEqual(r));
- }
- const EMPTY_DOCUMENT_VERSION_MAP = new SortedMap(DocumentKey.comparator);
- function documentVersionMap() {
- return EMPTY_DOCUMENT_VERSION_MAP;
- }
- const EMPTY_DOCUMENT_KEY_SET = new SortedSet(DocumentKey.comparator);
- function documentKeySet(...keys) {
- let set = EMPTY_DOCUMENT_KEY_SET;
- for (const key of keys) {
- set = set.add(key);
- }
- return set;
- }
- const EMPTY_TARGET_ID_SET = new SortedSet(primitiveComparator);
- function targetIdSet() {
- return EMPTY_TARGET_ID_SET;
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Returns an DoubleValue for `value` that is encoded based the serializer's
- * `useProto3Json` setting.
- */
- function toDouble(serializer, value) {
- if (serializer.useProto3Json) {
- if (isNaN(value)) {
- return { doubleValue: 'NaN' };
- }
- else if (value === Infinity) {
- return { doubleValue: 'Infinity' };
- }
- else if (value === -Infinity) {
- return { doubleValue: '-Infinity' };
- }
- }
- return { doubleValue: isNegativeZero(value) ? '-0' : value };
- }
- /**
- * Returns an IntegerValue for `value`.
- */
- function toInteger(value) {
- return { integerValue: '' + value };
- }
- /**
- * Returns a value for a number that's appropriate to put into a proto.
- * The return value is an IntegerValue if it can safely represent the value,
- * otherwise a DoubleValue is returned.
- */
- function toNumber(serializer, value) {
- return isSafeInteger(value) ? toInteger(value) : toDouble(serializer, value);
- }
- /**
- * @license
- * Copyright 2018 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /** Used to represent a field transform on a mutation. */
- class TransformOperation {
- constructor() {
- // Make sure that the structural type of `TransformOperation` is unique.
- // See https://github.com/microsoft/TypeScript/issues/5451
- this._ = undefined;
- }
- }
- /**
- * Computes the local transform result against the provided `previousValue`,
- * optionally using the provided localWriteTime.
- */
- function applyTransformOperationToLocalView(transform, previousValue, localWriteTime) {
- if (transform instanceof ServerTimestampTransform) {
- return serverTimestamp$1(localWriteTime, previousValue);
- }
- else if (transform instanceof ArrayUnionTransformOperation) {
- return applyArrayUnionTransformOperation(transform, previousValue);
- }
- else if (transform instanceof ArrayRemoveTransformOperation) {
- return applyArrayRemoveTransformOperation(transform, previousValue);
- }
- else {
- return applyNumericIncrementTransformOperationToLocalView(transform, previousValue);
- }
- }
- /**
- * Computes a final transform result after the transform has been acknowledged
- * by the server, potentially using the server-provided transformResult.
- */
- function applyTransformOperationToRemoteDocument(transform, previousValue, transformResult) {
- // The server just sends null as the transform result for array operations,
- // so we have to calculate a result the same as we do for local
- // applications.
- if (transform instanceof ArrayUnionTransformOperation) {
- return applyArrayUnionTransformOperation(transform, previousValue);
- }
- else if (transform instanceof ArrayRemoveTransformOperation) {
- return applyArrayRemoveTransformOperation(transform, previousValue);
- }
- return transformResult;
- }
- /**
- * If this transform operation is not idempotent, returns the base value to
- * persist for this transform. If a base value is returned, the transform
- * operation is always applied to this base value, even if document has
- * already been updated.
- *
- * Base values provide consistent behavior for non-idempotent transforms and
- * allow us to return the same latency-compensated value even if the backend
- * has already applied the transform operation. The base value is null for
- * idempotent transforms, as they can be re-played even if the backend has
- * already applied them.
- *
- * @returns a base value to store along with the mutation, or null for
- * idempotent transforms.
- */
- function computeTransformOperationBaseValue(transform, previousValue) {
- if (transform instanceof NumericIncrementTransformOperation) {
- return isNumber(previousValue) ? previousValue : { integerValue: 0 };
- }
- return null;
- }
- function transformOperationEquals(left, right) {
- if (left instanceof ArrayUnionTransformOperation &&
- right instanceof ArrayUnionTransformOperation) {
- return arrayEquals(left.elements, right.elements, valueEquals);
- }
- else if (left instanceof ArrayRemoveTransformOperation &&
- right instanceof ArrayRemoveTransformOperation) {
- return arrayEquals(left.elements, right.elements, valueEquals);
- }
- else if (left instanceof NumericIncrementTransformOperation &&
- right instanceof NumericIncrementTransformOperation) {
- return valueEquals(left.operand, right.operand);
- }
- return (left instanceof ServerTimestampTransform &&
- right instanceof ServerTimestampTransform);
- }
- /** Transforms a value into a server-generated timestamp. */
- class ServerTimestampTransform extends TransformOperation {
- }
- /** Transforms an array value via a union operation. */
- class ArrayUnionTransformOperation extends TransformOperation {
- constructor(elements) {
- super();
- this.elements = elements;
- }
- }
- function applyArrayUnionTransformOperation(transform, previousValue) {
- const values = coercedFieldValuesArray(previousValue);
- for (const toUnion of transform.elements) {
- if (!values.some(element => valueEquals(element, toUnion))) {
- values.push(toUnion);
- }
- }
- return { arrayValue: { values } };
- }
- /** Transforms an array value via a remove operation. */
- class ArrayRemoveTransformOperation extends TransformOperation {
- constructor(elements) {
- super();
- this.elements = elements;
- }
- }
- function applyArrayRemoveTransformOperation(transform, previousValue) {
- let values = coercedFieldValuesArray(previousValue);
- for (const toRemove of transform.elements) {
- values = values.filter(element => !valueEquals(element, toRemove));
- }
- return { arrayValue: { values } };
- }
- /**
- * Implements the backend semantics for locally computed NUMERIC_ADD (increment)
- * transforms. Converts all field values to integers or doubles, but unlike the
- * backend does not cap integer values at 2^63. Instead, JavaScript number
- * arithmetic is used and precision loss can occur for values greater than 2^53.
- */
- class NumericIncrementTransformOperation extends TransformOperation {
- constructor(serializer, operand) {
- super();
- this.serializer = serializer;
- this.operand = operand;
- }
- }
- function applyNumericIncrementTransformOperationToLocalView(transform, previousValue) {
- // PORTING NOTE: Since JavaScript's integer arithmetic is limited to 53 bit
- // precision and resolves overflows by reducing precision, we do not
- // manually cap overflows at 2^63.
- const baseValue = computeTransformOperationBaseValue(transform, previousValue);
- const sum = asNumber(baseValue) + asNumber(transform.operand);
- if (isInteger(baseValue) && isInteger(transform.operand)) {
- return toInteger(sum);
- }
- else {
- return toDouble(transform.serializer, sum);
- }
- }
- function asNumber(value) {
- return normalizeNumber(value.integerValue || value.doubleValue);
- }
- function coercedFieldValuesArray(value) {
- return isArray(value) && value.arrayValue.values
- ? value.arrayValue.values.slice()
- : [];
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /** A field path and the TransformOperation to perform upon it. */
- class FieldTransform {
- constructor(field, transform) {
- this.field = field;
- this.transform = transform;
- }
- }
- function fieldTransformEquals(left, right) {
- return (left.field.isEqual(right.field) &&
- transformOperationEquals(left.transform, right.transform));
- }
- function fieldTransformsAreEqual(left, right) {
- if (left === undefined && right === undefined) {
- return true;
- }
- if (left && right) {
- return arrayEquals(left, right, (l, r) => fieldTransformEquals(l, r));
- }
- return false;
- }
- /** The result of successfully applying a mutation to the backend. */
- class MutationResult {
- constructor(
- /**
- * The version at which the mutation was committed:
- *
- * - For most operations, this is the updateTime in the WriteResult.
- * - For deletes, the commitTime of the WriteResponse (because deletes are
- * not stored and have no updateTime).
- *
- * Note that these versions can be different: No-op writes will not change
- * the updateTime even though the commitTime advances.
- */
- version,
- /**
- * The resulting fields returned from the backend after a mutation
- * containing field transforms has been committed. Contains one FieldValue
- * for each FieldTransform that was in the mutation.
- *
- * Will be empty if the mutation did not contain any field transforms.
- */
- transformResults) {
- this.version = version;
- this.transformResults = transformResults;
- }
- }
- /**
- * Encodes a precondition for a mutation. This follows the model that the
- * backend accepts with the special case of an explicit "empty" precondition
- * (meaning no precondition).
- */
- class Precondition {
- constructor(updateTime, exists) {
- this.updateTime = updateTime;
- this.exists = exists;
- }
- /** Creates a new empty Precondition. */
- static none() {
- return new Precondition();
- }
- /** Creates a new Precondition with an exists flag. */
- static exists(exists) {
- return new Precondition(undefined, exists);
- }
- /** Creates a new Precondition based on a version a document exists at. */
- static updateTime(version) {
- return new Precondition(version);
- }
- /** Returns whether this Precondition is empty. */
- get isNone() {
- return this.updateTime === undefined && this.exists === undefined;
- }
- isEqual(other) {
- return (this.exists === other.exists &&
- (this.updateTime
- ? !!other.updateTime && this.updateTime.isEqual(other.updateTime)
- : !other.updateTime));
- }
- }
- /** Returns true if the preconditions is valid for the given document. */
- function preconditionIsValidForDocument(precondition, document) {
- if (precondition.updateTime !== undefined) {
- return (document.isFoundDocument() &&
- document.version.isEqual(precondition.updateTime));
- }
- else if (precondition.exists !== undefined) {
- return precondition.exists === document.isFoundDocument();
- }
- else {
- return true;
- }
- }
- /**
- * A mutation describes a self-contained change to a document. Mutations can
- * create, replace, delete, and update subsets of documents.
- *
- * Mutations not only act on the value of the document but also its version.
- *
- * For local mutations (mutations that haven't been committed yet), we preserve
- * the existing version for Set and Patch mutations. For Delete mutations, we
- * reset the version to 0.
- *
- * Here's the expected transition table.
- *
- * MUTATION APPLIED TO RESULTS IN
- *
- * SetMutation Document(v3) Document(v3)
- * SetMutation NoDocument(v3) Document(v0)
- * SetMutation InvalidDocument(v0) Document(v0)
- * PatchMutation Document(v3) Document(v3)
- * PatchMutation NoDocument(v3) NoDocument(v3)
- * PatchMutation InvalidDocument(v0) UnknownDocument(v3)
- * DeleteMutation Document(v3) NoDocument(v0)
- * DeleteMutation NoDocument(v3) NoDocument(v0)
- * DeleteMutation InvalidDocument(v0) NoDocument(v0)
- *
- * For acknowledged mutations, we use the updateTime of the WriteResponse as
- * the resulting version for Set and Patch mutations. As deletes have no
- * explicit update time, we use the commitTime of the WriteResponse for
- * Delete mutations.
- *
- * If a mutation is acknowledged by the backend but fails the precondition check
- * locally, we transition to an `UnknownDocument` and rely on Watch to send us
- * the updated version.
- *
- * Field transforms are used only with Patch and Set Mutations. We use the
- * `updateTransforms` message to store transforms, rather than the `transforms`s
- * messages.
- *
- * ## Subclassing Notes
- *
- * Every type of mutation needs to implement its own applyToRemoteDocument() and
- * applyToLocalView() to implement the actual behavior of applying the mutation
- * to some source document (see `setMutationApplyToRemoteDocument()` for an
- * example).
- */
- class Mutation {
- }
- /**
- * A utility method to calculate a `Mutation` representing the overlay from the
- * final state of the document, and a `FieldMask` representing the fields that
- * are mutated by the local mutations.
- */
- function calculateOverlayMutation(doc, mask) {
- if (!doc.hasLocalMutations || (mask && mask.fields.length === 0)) {
- return null;
- }
- // mask is null when sets or deletes are applied to the current document.
- if (mask === null) {
- if (doc.isNoDocument()) {
- return new DeleteMutation(doc.key, Precondition.none());
- }
- else {
- return new SetMutation(doc.key, doc.data, Precondition.none());
- }
- }
- else {
- const docValue = doc.data;
- const patchValue = ObjectValue.empty();
- let maskSet = new SortedSet(FieldPath$1.comparator);
- for (let path of mask.fields) {
- if (!maskSet.has(path)) {
- let value = docValue.field(path);
- // If we are deleting a nested field, we take the immediate parent as
- // the mask used to construct the resulting mutation.
- // Justification: Nested fields can create parent fields implicitly. If
- // only a leaf entry is deleted in later mutations, the parent field
- // should still remain, but we may have lost this information.
- // Consider mutation (foo.bar 1), then mutation (foo.bar delete()).
- // This leaves the final result (foo, {}). Despite the fact that `doc`
- // has the correct result, `foo` is not in `mask`, and the resulting
- // mutation would miss `foo`.
- if (value === null && path.length > 1) {
- path = path.popLast();
- value = docValue.field(path);
- }
- if (value === null) {
- patchValue.delete(path);
- }
- else {
- patchValue.set(path, value);
- }
- maskSet = maskSet.add(path);
- }
- }
- return new PatchMutation(doc.key, patchValue, new FieldMask(maskSet.toArray()), Precondition.none());
- }
- }
- /**
- * Applies this mutation to the given document for the purposes of computing a
- * new remote document. If the input document doesn't match the expected state
- * (e.g. it is invalid or outdated), the document type may transition to
- * unknown.
- *
- * @param mutation - The mutation to apply.
- * @param document - The document to mutate. The input document can be an
- * invalid document if the client has no knowledge of the pre-mutation state
- * of the document.
- * @param mutationResult - The result of applying the mutation from the backend.
- */
- function mutationApplyToRemoteDocument(mutation, document, mutationResult) {
- if (mutation instanceof SetMutation) {
- setMutationApplyToRemoteDocument(mutation, document, mutationResult);
- }
- else if (mutation instanceof PatchMutation) {
- patchMutationApplyToRemoteDocument(mutation, document, mutationResult);
- }
- else {
- deleteMutationApplyToRemoteDocument(mutation, document, mutationResult);
- }
- }
- /**
- * Applies this mutation to the given document for the purposes of computing
- * the new local view of a document. If the input document doesn't match the
- * expected state, the document is not modified.
- *
- * @param mutation - The mutation to apply.
- * @param document - The document to mutate. The input document can be an
- * invalid document if the client has no knowledge of the pre-mutation state
- * of the document.
- * @param previousMask - The fields that have been updated before applying this mutation.
- * @param localWriteTime - A timestamp indicating the local write time of the
- * batch this mutation is a part of.
- * @returns A `FieldMask` representing the fields that are changed by applying this mutation.
- */
- function mutationApplyToLocalView(mutation, document, previousMask, localWriteTime) {
- if (mutation instanceof SetMutation) {
- return setMutationApplyToLocalView(mutation, document, previousMask, localWriteTime);
- }
- else if (mutation instanceof PatchMutation) {
- return patchMutationApplyToLocalView(mutation, document, previousMask, localWriteTime);
- }
- else {
- return deleteMutationApplyToLocalView(mutation, document, previousMask);
- }
- }
- /**
- * If this mutation is not idempotent, returns the base value to persist with
- * this mutation. If a base value is returned, the mutation is always applied
- * to this base value, even if document has already been updated.
- *
- * The base value is a sparse object that consists of only the document
- * fields for which this mutation contains a non-idempotent transformation
- * (e.g. a numeric increment). The provided value guarantees consistent
- * behavior for non-idempotent transforms and allow us to return the same
- * latency-compensated value even if the backend has already applied the
- * mutation. The base value is null for idempotent mutations, as they can be
- * re-played even if the backend has already applied them.
- *
- * @returns a base value to store along with the mutation, or null for
- * idempotent mutations.
- */
- function mutationExtractBaseValue(mutation, document) {
- let baseObject = null;
- for (const fieldTransform of mutation.fieldTransforms) {
- const existingValue = document.data.field(fieldTransform.field);
- const coercedValue = computeTransformOperationBaseValue(fieldTransform.transform, existingValue || null);
- if (coercedValue != null) {
- if (baseObject === null) {
- baseObject = ObjectValue.empty();
- }
- baseObject.set(fieldTransform.field, coercedValue);
- }
- }
- return baseObject ? baseObject : null;
- }
- function mutationEquals(left, right) {
- if (left.type !== right.type) {
- return false;
- }
- if (!left.key.isEqual(right.key)) {
- return false;
- }
- if (!left.precondition.isEqual(right.precondition)) {
- return false;
- }
- if (!fieldTransformsAreEqual(left.fieldTransforms, right.fieldTransforms)) {
- return false;
- }
- if (left.type === 0 /* MutationType.Set */) {
- return left.value.isEqual(right.value);
- }
- if (left.type === 1 /* MutationType.Patch */) {
- return (left.data.isEqual(right.data) &&
- left.fieldMask.isEqual(right.fieldMask));
- }
- return true;
- }
- /**
- * A mutation that creates or replaces the document at the given key with the
- * object value contents.
- */
- class SetMutation extends Mutation {
- constructor(key, value, precondition, fieldTransforms = []) {
- super();
- this.key = key;
- this.value = value;
- this.precondition = precondition;
- this.fieldTransforms = fieldTransforms;
- this.type = 0 /* MutationType.Set */;
- }
- getFieldMask() {
- return null;
- }
- }
- function setMutationApplyToRemoteDocument(mutation, document, mutationResult) {
- // Unlike setMutationApplyToLocalView, if we're applying a mutation to a
- // remote document the server has accepted the mutation so the precondition
- // must have held.
- const newData = mutation.value.clone();
- const transformResults = serverTransformResults(mutation.fieldTransforms, document, mutationResult.transformResults);
- newData.setAll(transformResults);
- document
- .convertToFoundDocument(mutationResult.version, newData)
- .setHasCommittedMutations();
- }
- function setMutationApplyToLocalView(mutation, document, previousMask, localWriteTime) {
- if (!preconditionIsValidForDocument(mutation.precondition, document)) {
- // The mutation failed to apply (e.g. a document ID created with add()
- // caused a name collision).
- return previousMask;
- }
- const newData = mutation.value.clone();
- const transformResults = localTransformResults(mutation.fieldTransforms, localWriteTime, document);
- newData.setAll(transformResults);
- document
- .convertToFoundDocument(document.version, newData)
- .setHasLocalMutations();
- return null; // SetMutation overwrites all fields.
- }
- /**
- * A mutation that modifies fields of the document at the given key with the
- * given values. The values are applied through a field mask:
- *
- * * When a field is in both the mask and the values, the corresponding field
- * is updated.
- * * When a field is in neither the mask nor the values, the corresponding
- * field is unmodified.
- * * When a field is in the mask but not in the values, the corresponding field
- * is deleted.
- * * When a field is not in the mask but is in the values, the values map is
- * ignored.
- */
- class PatchMutation extends Mutation {
- constructor(key, data, fieldMask, precondition, fieldTransforms = []) {
- super();
- this.key = key;
- this.data = data;
- this.fieldMask = fieldMask;
- this.precondition = precondition;
- this.fieldTransforms = fieldTransforms;
- this.type = 1 /* MutationType.Patch */;
- }
- getFieldMask() {
- return this.fieldMask;
- }
- }
- function patchMutationApplyToRemoteDocument(mutation, document, mutationResult) {
- if (!preconditionIsValidForDocument(mutation.precondition, document)) {
- // Since the mutation was not rejected, we know that the precondition
- // matched on the backend. We therefore must not have the expected version
- // of the document in our cache and convert to an UnknownDocument with a
- // known updateTime.
- document.convertToUnknownDocument(mutationResult.version);
- return;
- }
- const transformResults = serverTransformResults(mutation.fieldTransforms, document, mutationResult.transformResults);
- const newData = document.data;
- newData.setAll(getPatch(mutation));
- newData.setAll(transformResults);
- document
- .convertToFoundDocument(mutationResult.version, newData)
- .setHasCommittedMutations();
- }
- function patchMutationApplyToLocalView(mutation, document, previousMask, localWriteTime) {
- if (!preconditionIsValidForDocument(mutation.precondition, document)) {
- return previousMask;
- }
- const transformResults = localTransformResults(mutation.fieldTransforms, localWriteTime, document);
- const newData = document.data;
- newData.setAll(getPatch(mutation));
- newData.setAll(transformResults);
- document
- .convertToFoundDocument(document.version, newData)
- .setHasLocalMutations();
- if (previousMask === null) {
- return null;
- }
- return previousMask
- .unionWith(mutation.fieldMask.fields)
- .unionWith(mutation.fieldTransforms.map(transform => transform.field));
- }
- /**
- * Returns a FieldPath/Value map with the content of the PatchMutation.
- */
- function getPatch(mutation) {
- const result = new Map();
- mutation.fieldMask.fields.forEach(fieldPath => {
- if (!fieldPath.isEmpty()) {
- const newValue = mutation.data.field(fieldPath);
- result.set(fieldPath, newValue);
- }
- });
- return result;
- }
- /**
- * Creates a list of "transform results" (a transform result is a field value
- * representing the result of applying a transform) for use after a mutation
- * containing transforms has been acknowledged by the server.
- *
- * @param fieldTransforms - The field transforms to apply the result to.
- * @param mutableDocument - The current state of the document after applying all
- * previous mutations.
- * @param serverTransformResults - The transform results received by the server.
- * @returns The transform results list.
- */
- function serverTransformResults(fieldTransforms, mutableDocument, serverTransformResults) {
- const transformResults = new Map();
- hardAssert(fieldTransforms.length === serverTransformResults.length);
- for (let i = 0; i < serverTransformResults.length; i++) {
- const fieldTransform = fieldTransforms[i];
- const transform = fieldTransform.transform;
- const previousValue = mutableDocument.data.field(fieldTransform.field);
- transformResults.set(fieldTransform.field, applyTransformOperationToRemoteDocument(transform, previousValue, serverTransformResults[i]));
- }
- return transformResults;
- }
- /**
- * Creates a list of "transform results" (a transform result is a field value
- * representing the result of applying a transform) for use when applying a
- * transform locally.
- *
- * @param fieldTransforms - The field transforms to apply the result to.
- * @param localWriteTime - The local time of the mutation (used to
- * generate ServerTimestampValues).
- * @param mutableDocument - The document to apply transforms on.
- * @returns The transform results list.
- */
- function localTransformResults(fieldTransforms, localWriteTime, mutableDocument) {
- const transformResults = new Map();
- for (const fieldTransform of fieldTransforms) {
- const transform = fieldTransform.transform;
- const previousValue = mutableDocument.data.field(fieldTransform.field);
- transformResults.set(fieldTransform.field, applyTransformOperationToLocalView(transform, previousValue, localWriteTime));
- }
- return transformResults;
- }
- /** A mutation that deletes the document at the given key. */
- class DeleteMutation extends Mutation {
- constructor(key, precondition) {
- super();
- this.key = key;
- this.precondition = precondition;
- this.type = 2 /* MutationType.Delete */;
- this.fieldTransforms = [];
- }
- getFieldMask() {
- return null;
- }
- }
- function deleteMutationApplyToRemoteDocument(mutation, document, mutationResult) {
- // Unlike applyToLocalView, if we're applying a mutation to a remote
- // document the server has accepted the mutation so the precondition must
- // have held.
- document
- .convertToNoDocument(mutationResult.version)
- .setHasCommittedMutations();
- }
- function deleteMutationApplyToLocalView(mutation, document, previousMask) {
- if (preconditionIsValidForDocument(mutation.precondition, document)) {
- document.convertToNoDocument(document.version).setHasLocalMutations();
- return null;
- }
- return previousMask;
- }
- /**
- * A mutation that verifies the existence of the document at the given key with
- * the provided precondition.
- *
- * The `verify` operation is only used in Transactions, and this class serves
- * primarily to facilitate serialization into protos.
- */
- class VerifyMutation extends Mutation {
- constructor(key, precondition) {
- super();
- this.key = key;
- this.precondition = precondition;
- this.type = 3 /* MutationType.Verify */;
- this.fieldTransforms = [];
- }
- getFieldMask() {
- return null;
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * A batch of mutations that will be sent as one unit to the backend.
- */
- class MutationBatch {
- /**
- * @param batchId - The unique ID of this mutation batch.
- * @param localWriteTime - The original write time of this mutation.
- * @param baseMutations - Mutations that are used to populate the base
- * values when this mutation is applied locally. This can be used to locally
- * overwrite values that are persisted in the remote document cache. Base
- * mutations are never sent to the backend.
- * @param mutations - The user-provided mutations in this mutation batch.
- * User-provided mutations are applied both locally and remotely on the
- * backend.
- */
- constructor(batchId, localWriteTime, baseMutations, mutations) {
- this.batchId = batchId;
- this.localWriteTime = localWriteTime;
- this.baseMutations = baseMutations;
- this.mutations = mutations;
- }
- /**
- * Applies all the mutations in this MutationBatch to the specified document
- * to compute the state of the remote document
- *
- * @param document - The document to apply mutations to.
- * @param batchResult - The result of applying the MutationBatch to the
- * backend.
- */
- applyToRemoteDocument(document, batchResult) {
- const mutationResults = batchResult.mutationResults;
- for (let i = 0; i < this.mutations.length; i++) {
- const mutation = this.mutations[i];
- if (mutation.key.isEqual(document.key)) {
- const mutationResult = mutationResults[i];
- mutationApplyToRemoteDocument(mutation, document, mutationResult);
- }
- }
- }
- /**
- * Computes the local view of a document given all the mutations in this
- * batch.
- *
- * @param document - The document to apply mutations to.
- * @param mutatedFields - Fields that have been updated before applying this mutation batch.
- * @returns A `FieldMask` representing all the fields that are mutated.
- */
- applyToLocalView(document, mutatedFields) {
- // First, apply the base state. This allows us to apply non-idempotent
- // transform against a consistent set of values.
- for (const mutation of this.baseMutations) {
- if (mutation.key.isEqual(document.key)) {
- mutatedFields = mutationApplyToLocalView(mutation, document, mutatedFields, this.localWriteTime);
- }
- }
- // Second, apply all user-provided mutations.
- for (const mutation of this.mutations) {
- if (mutation.key.isEqual(document.key)) {
- mutatedFields = mutationApplyToLocalView(mutation, document, mutatedFields, this.localWriteTime);
- }
- }
- return mutatedFields;
- }
- /**
- * Computes the local view for all provided documents given the mutations in
- * this batch. Returns a `DocumentKey` to `Mutation` map which can be used to
- * replace all the mutation applications.
- */
- applyToLocalDocumentSet(documentMap, documentsWithoutRemoteVersion) {
- // TODO(mrschmidt): This implementation is O(n^2). If we apply the mutations
- // directly (as done in `applyToLocalView()`), we can reduce the complexity
- // to O(n).
- const overlays = newMutationMap();
- this.mutations.forEach(m => {
- const overlayedDocument = documentMap.get(m.key);
- // TODO(mutabledocuments): This method should take a MutableDocumentMap
- // and we should remove this cast.
- const mutableDocument = overlayedDocument.overlayedDocument;
- let mutatedFields = this.applyToLocalView(mutableDocument, overlayedDocument.mutatedFields);
- // Set mutatedFields to null if the document is only from local mutations.
- // This creates a Set or Delete mutation, instead of trying to create a
- // patch mutation as the overlay.
- mutatedFields = documentsWithoutRemoteVersion.has(m.key)
- ? null
- : mutatedFields;
- const overlay = calculateOverlayMutation(mutableDocument, mutatedFields);
- if (overlay !== null) {
- overlays.set(m.key, overlay);
- }
- if (!mutableDocument.isValidDocument()) {
- mutableDocument.convertToNoDocument(SnapshotVersion.min());
- }
- });
- return overlays;
- }
- keys() {
- return this.mutations.reduce((keys, m) => keys.add(m.key), documentKeySet());
- }
- isEqual(other) {
- return (this.batchId === other.batchId &&
- arrayEquals(this.mutations, other.mutations, (l, r) => mutationEquals(l, r)) &&
- arrayEquals(this.baseMutations, other.baseMutations, (l, r) => mutationEquals(l, r)));
- }
- }
- /** The result of applying a mutation batch to the backend. */
- class MutationBatchResult {
- constructor(batch, commitVersion, mutationResults,
- /**
- * A pre-computed mapping from each mutated document to the resulting
- * version.
- */
- docVersions) {
- this.batch = batch;
- this.commitVersion = commitVersion;
- this.mutationResults = mutationResults;
- this.docVersions = docVersions;
- }
- /**
- * Creates a new MutationBatchResult for the given batch and results. There
- * must be one result for each mutation in the batch. This static factory
- * caches a document=>version mapping (docVersions).
- */
- static from(batch, commitVersion, results) {
- hardAssert(batch.mutations.length === results.length);
- let versionMap = documentVersionMap();
- const mutations = batch.mutations;
- for (let i = 0; i < mutations.length; i++) {
- versionMap = versionMap.insert(mutations[i].key, results[i].version);
- }
- return new MutationBatchResult(batch, commitVersion, results, versionMap);
- }
- }
- /**
- * @license
- * Copyright 2022 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Representation of an overlay computed by Firestore.
- *
- * Holds information about a mutation and the largest batch id in Firestore when
- * the mutation was created.
- */
- class Overlay {
- constructor(largestBatchId, mutation) {
- this.largestBatchId = largestBatchId;
- this.mutation = mutation;
- }
- getKey() {
- return this.mutation.key;
- }
- isEqual(other) {
- return other !== null && this.mutation === other.mutation;
- }
- toString() {
- return `Overlay{
- largestBatchId: ${this.largestBatchId},
- mutation: ${this.mutation.toString()}
- }`;
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- class ExistenceFilter {
- constructor(count, unchangedNames) {
- this.count = count;
- this.unchangedNames = unchangedNames;
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Error Codes describing the different ways GRPC can fail. These are copied
- * directly from GRPC's sources here:
- *
- * https://github.com/grpc/grpc/blob/bceec94ea4fc5f0085d81235d8e1c06798dc341a/include/grpc%2B%2B/impl/codegen/status_code_enum.h
- *
- * Important! The names of these identifiers matter because the string forms
- * are used for reverse lookups from the webchannel stream. Do NOT change the
- * names of these identifiers or change this into a const enum.
- */
- var RpcCode;
- (function (RpcCode) {
- RpcCode[RpcCode["OK"] = 0] = "OK";
- RpcCode[RpcCode["CANCELLED"] = 1] = "CANCELLED";
- RpcCode[RpcCode["UNKNOWN"] = 2] = "UNKNOWN";
- RpcCode[RpcCode["INVALID_ARGUMENT"] = 3] = "INVALID_ARGUMENT";
- RpcCode[RpcCode["DEADLINE_EXCEEDED"] = 4] = "DEADLINE_EXCEEDED";
- RpcCode[RpcCode["NOT_FOUND"] = 5] = "NOT_FOUND";
- RpcCode[RpcCode["ALREADY_EXISTS"] = 6] = "ALREADY_EXISTS";
- RpcCode[RpcCode["PERMISSION_DENIED"] = 7] = "PERMISSION_DENIED";
- RpcCode[RpcCode["UNAUTHENTICATED"] = 16] = "UNAUTHENTICATED";
- RpcCode[RpcCode["RESOURCE_EXHAUSTED"] = 8] = "RESOURCE_EXHAUSTED";
- RpcCode[RpcCode["FAILED_PRECONDITION"] = 9] = "FAILED_PRECONDITION";
- RpcCode[RpcCode["ABORTED"] = 10] = "ABORTED";
- RpcCode[RpcCode["OUT_OF_RANGE"] = 11] = "OUT_OF_RANGE";
- RpcCode[RpcCode["UNIMPLEMENTED"] = 12] = "UNIMPLEMENTED";
- RpcCode[RpcCode["INTERNAL"] = 13] = "INTERNAL";
- RpcCode[RpcCode["UNAVAILABLE"] = 14] = "UNAVAILABLE";
- RpcCode[RpcCode["DATA_LOSS"] = 15] = "DATA_LOSS";
- })(RpcCode || (RpcCode = {}));
- /**
- * Determines whether an error code represents a permanent error when received
- * in response to a non-write operation.
- *
- * See isPermanentWriteError for classifying write errors.
- */
- function isPermanentError(code) {
- switch (code) {
- case Code.OK:
- return fail();
- case Code.CANCELLED:
- case Code.UNKNOWN:
- case Code.DEADLINE_EXCEEDED:
- case Code.RESOURCE_EXHAUSTED:
- case Code.INTERNAL:
- case Code.UNAVAILABLE:
- // Unauthenticated means something went wrong with our token and we need
- // to retry with new credentials which will happen automatically.
- case Code.UNAUTHENTICATED:
- return false;
- case Code.INVALID_ARGUMENT:
- case Code.NOT_FOUND:
- case Code.ALREADY_EXISTS:
- case Code.PERMISSION_DENIED:
- case Code.FAILED_PRECONDITION:
- // Aborted might be retried in some scenarios, but that is dependant on
- // the context and should handled individually by the calling code.
- // See https://cloud.google.com/apis/design/errors.
- case Code.ABORTED:
- case Code.OUT_OF_RANGE:
- case Code.UNIMPLEMENTED:
- case Code.DATA_LOSS:
- return true;
- default:
- return fail();
- }
- }
- /**
- * Determines whether an error code represents a permanent error when received
- * in response to a write operation.
- *
- * Write operations must be handled specially because as of b/119437764, ABORTED
- * errors on the write stream should be retried too (even though ABORTED errors
- * are not generally retryable).
- *
- * Note that during the initial handshake on the write stream an ABORTED error
- * signals that we should discard our stream token (i.e. it is permanent). This
- * means a handshake error should be classified with isPermanentError, above.
- */
- function isPermanentWriteError(code) {
- return isPermanentError(code) && code !== Code.ABORTED;
- }
- /**
- * Maps an error Code from GRPC status code number, like 0, 1, or 14. These
- * are not the same as HTTP status codes.
- *
- * @returns The Code equivalent to the given GRPC status code. Fails if there
- * is no match.
- */
- function mapCodeFromRpcCode(code) {
- if (code === undefined) {
- // This shouldn't normally happen, but in certain error cases (like trying
- // to send invalid proto messages) we may get an error with no GRPC code.
- logError('GRPC error has no .code');
- return Code.UNKNOWN;
- }
- switch (code) {
- case RpcCode.OK:
- return Code.OK;
- case RpcCode.CANCELLED:
- return Code.CANCELLED;
- case RpcCode.UNKNOWN:
- return Code.UNKNOWN;
- case RpcCode.DEADLINE_EXCEEDED:
- return Code.DEADLINE_EXCEEDED;
- case RpcCode.RESOURCE_EXHAUSTED:
- return Code.RESOURCE_EXHAUSTED;
- case RpcCode.INTERNAL:
- return Code.INTERNAL;
- case RpcCode.UNAVAILABLE:
- return Code.UNAVAILABLE;
- case RpcCode.UNAUTHENTICATED:
- return Code.UNAUTHENTICATED;
- case RpcCode.INVALID_ARGUMENT:
- return Code.INVALID_ARGUMENT;
- case RpcCode.NOT_FOUND:
- return Code.NOT_FOUND;
- case RpcCode.ALREADY_EXISTS:
- return Code.ALREADY_EXISTS;
- case RpcCode.PERMISSION_DENIED:
- return Code.PERMISSION_DENIED;
- case RpcCode.FAILED_PRECONDITION:
- return Code.FAILED_PRECONDITION;
- case RpcCode.ABORTED:
- return Code.ABORTED;
- case RpcCode.OUT_OF_RANGE:
- return Code.OUT_OF_RANGE;
- case RpcCode.UNIMPLEMENTED:
- return Code.UNIMPLEMENTED;
- case RpcCode.DATA_LOSS:
- return Code.DATA_LOSS;
- default:
- return fail();
- }
- }
- /**
- * @license
- * Copyright 2023 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * An error encountered while decoding base64 string.
- */
- class Base64DecodeError extends Error {
- constructor() {
- super(...arguments);
- this.name = 'Base64DecodeError';
- }
- }
- /**
- * @license
- * Copyright 2023 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Manages "testing hooks", hooks into the internals of the SDK to verify
- * internal state and events during integration tests. Do not use this class
- * except for testing purposes.
- *
- * There are two ways to retrieve the global singleton instance of this class:
- * 1. The `instance` property, which returns null if the global singleton
- * instance has not been created. Use this property if the caller should
- * "do nothing" if there are no testing hooks registered, such as when
- * delivering an event to notify registered callbacks.
- * 2. The `getOrCreateInstance()` method, which creates the global singleton
- * instance if it has not been created. Use this method if the instance is
- * needed to, for example, register a callback.
- *
- * @internal
- */
- class TestingHooks {
- constructor() {
- this.onExistenceFilterMismatchCallbacks = new Map();
- }
- /**
- * Returns the singleton instance of this class, or null if it has not been
- * initialized.
- */
- static get instance() {
- return gTestingHooksSingletonInstance;
- }
- /**
- * Returns the singleton instance of this class, creating it if is has never
- * been created before.
- */
- static getOrCreateInstance() {
- if (gTestingHooksSingletonInstance === null) {
- gTestingHooksSingletonInstance = new TestingHooks();
- }
- return gTestingHooksSingletonInstance;
- }
- /**
- * Registers a callback to be notified when an existence filter mismatch
- * occurs in the Watch listen stream.
- *
- * The relative order in which callbacks are notified is unspecified; do not
- * rely on any particular ordering. If a given callback is registered multiple
- * times then it will be notified multiple times, once per registration.
- *
- * @param callback the callback to invoke upon existence filter mismatch.
- *
- * @return a function that, when called, unregisters the given callback; only
- * the first invocation of the returned function does anything; all subsequent
- * invocations do nothing.
- */
- onExistenceFilterMismatch(callback) {
- const key = Symbol();
- this.onExistenceFilterMismatchCallbacks.set(key, callback);
- return () => this.onExistenceFilterMismatchCallbacks.delete(key);
- }
- /**
- * Invokes all currently-registered `onExistenceFilterMismatch` callbacks.
- * @param info Information about the existence filter mismatch.
- */
- notifyOnExistenceFilterMismatch(info) {
- this.onExistenceFilterMismatchCallbacks.forEach(callback => callback(info));
- }
- }
- /** The global singleton instance of `TestingHooks`. */
- let gTestingHooksSingletonInstance = null;
- /**
- * @license
- * Copyright 2023 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * An instance of the Platform's 'TextEncoder' implementation.
- */
- function newTextEncoder() {
- return new util$1.TextEncoder();
- }
- /**
- * An instance of the Platform's 'TextDecoder' implementation.
- */
- function newTextDecoder() {
- return new util$1.TextDecoder('utf-8');
- }
- /**
- * @license
- * Copyright 2022 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const MAX_64_BIT_UNSIGNED_INTEGER = new webchannelWrapper.Integer([0xffffffff, 0xffffffff], 0);
- // Hash a string using md5 hashing algorithm.
- function getMd5HashValue(value) {
- const encodedValue = newTextEncoder().encode(value);
- const md5 = new webchannelWrapper.Md5();
- md5.update(encodedValue);
- return new Uint8Array(md5.digest());
- }
- // Interpret the 16 bytes array as two 64-bit unsigned integers, encoded using
- // 2’s complement using little endian.
- function get64BitUints(Bytes) {
- const dataView = new DataView(Bytes.buffer);
- const chunk1 = dataView.getUint32(0, /* littleEndian= */ true);
- const chunk2 = dataView.getUint32(4, /* littleEndian= */ true);
- const chunk3 = dataView.getUint32(8, /* littleEndian= */ true);
- const chunk4 = dataView.getUint32(12, /* littleEndian= */ true);
- const integer1 = new webchannelWrapper.Integer([chunk1, chunk2], 0);
- const integer2 = new webchannelWrapper.Integer([chunk3, chunk4], 0);
- return [integer1, integer2];
- }
- class BloomFilter {
- constructor(bitmap, padding, hashCount) {
- this.bitmap = bitmap;
- this.padding = padding;
- this.hashCount = hashCount;
- if (padding < 0 || padding >= 8) {
- throw new BloomFilterError(`Invalid padding: ${padding}`);
- }
- if (hashCount < 0) {
- throw new BloomFilterError(`Invalid hash count: ${hashCount}`);
- }
- if (bitmap.length > 0 && this.hashCount === 0) {
- // Only empty bloom filter can have 0 hash count.
- throw new BloomFilterError(`Invalid hash count: ${hashCount}`);
- }
- if (bitmap.length === 0 && padding !== 0) {
- // Empty bloom filter should have 0 padding.
- throw new BloomFilterError(`Invalid padding when bitmap length is 0: ${padding}`);
- }
- this.bitCount = bitmap.length * 8 - padding;
- // Set the bit count in Integer to avoid repetition in mightContain().
- this.bitCountInInteger = webchannelWrapper.Integer.fromNumber(this.bitCount);
- }
- // Calculate the ith hash value based on the hashed 64bit integers,
- // and calculate its corresponding bit index in the bitmap to be checked.
- getBitIndex(num1, num2, hashIndex) {
- // Calculate hashed value h(i) = h1 + (i * h2).
- let hashValue = num1.add(num2.multiply(webchannelWrapper.Integer.fromNumber(hashIndex)));
- // Wrap if hash value overflow 64bit.
- if (hashValue.compare(MAX_64_BIT_UNSIGNED_INTEGER) === 1) {
- hashValue = new webchannelWrapper.Integer([hashValue.getBits(0), hashValue.getBits(1)], 0);
- }
- return hashValue.modulo(this.bitCountInInteger).toNumber();
- }
- // Return whether the bit on the given index in the bitmap is set to 1.
- isBitSet(index) {
- // To retrieve bit n, calculate: (bitmap[n / 8] & (0x01 << (n % 8))).
- const byte = this.bitmap[Math.floor(index / 8)];
- const offset = index % 8;
- return (byte & (0x01 << offset)) !== 0;
- }
- mightContain(value) {
- // Empty bitmap should always return false on membership check.
- if (this.bitCount === 0) {
- return false;
- }
- const md5HashedValue = getMd5HashValue(value);
- const [hash1, hash2] = get64BitUints(md5HashedValue);
- for (let i = 0; i < this.hashCount; i++) {
- const index = this.getBitIndex(hash1, hash2, i);
- if (!this.isBitSet(index)) {
- return false;
- }
- }
- return true;
- }
- /** Create bloom filter for testing purposes only. */
- static create(bitCount, hashCount, contains) {
- const padding = bitCount % 8 === 0 ? 0 : 8 - (bitCount % 8);
- const bitmap = new Uint8Array(Math.ceil(bitCount / 8));
- const bloomFilter = new BloomFilter(bitmap, padding, hashCount);
- contains.forEach(item => bloomFilter.insert(item));
- return bloomFilter;
- }
- insert(value) {
- if (this.bitCount === 0) {
- return;
- }
- const md5HashedValue = getMd5HashValue(value);
- const [hash1, hash2] = get64BitUints(md5HashedValue);
- for (let i = 0; i < this.hashCount; i++) {
- const index = this.getBitIndex(hash1, hash2, i);
- this.setBit(index);
- }
- }
- setBit(index) {
- const indexOfByte = Math.floor(index / 8);
- const offset = index % 8;
- this.bitmap[indexOfByte] |= 0x01 << offset;
- }
- }
- class BloomFilterError extends Error {
- constructor() {
- super(...arguments);
- this.name = 'BloomFilterError';
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * An event from the RemoteStore. It is split into targetChanges (changes to the
- * state or the set of documents in our watched targets) and documentUpdates
- * (changes to the actual documents).
- */
- class RemoteEvent {
- constructor(
- /**
- * The snapshot version this event brings us up to, or MIN if not set.
- */
- snapshotVersion,
- /**
- * A map from target to changes to the target. See TargetChange.
- */
- targetChanges,
- /**
- * A map of targets that is known to be inconsistent, and the purpose for
- * re-listening. Listens for these targets should be re-established without
- * resume tokens.
- */
- targetMismatches,
- /**
- * A set of which documents have changed or been deleted, along with the
- * doc's new values (if not deleted).
- */
- documentUpdates,
- /**
- * A set of which document updates are due only to limbo resolution targets.
- */
- resolvedLimboDocuments) {
- this.snapshotVersion = snapshotVersion;
- this.targetChanges = targetChanges;
- this.targetMismatches = targetMismatches;
- this.documentUpdates = documentUpdates;
- this.resolvedLimboDocuments = resolvedLimboDocuments;
- }
- /**
- * HACK: Views require RemoteEvents in order to determine whether the view is
- * CURRENT, but secondary tabs don't receive remote events. So this method is
- * used to create a synthesized RemoteEvent that can be used to apply a
- * CURRENT status change to a View, for queries executed in a different tab.
- */
- // PORTING NOTE: Multi-tab only
- static createSynthesizedRemoteEventForCurrentChange(targetId, current, resumeToken) {
- const targetChanges = new Map();
- targetChanges.set(targetId, TargetChange.createSynthesizedTargetChangeForCurrentChange(targetId, current, resumeToken));
- return new RemoteEvent(SnapshotVersion.min(), targetChanges, new SortedMap(primitiveComparator), mutableDocumentMap(), documentKeySet());
- }
- }
- /**
- * A TargetChange specifies the set of changes for a specific target as part of
- * a RemoteEvent. These changes track which documents are added, modified or
- * removed, as well as the target's resume token and whether the target is
- * marked CURRENT.
- * The actual changes *to* documents are not part of the TargetChange since
- * documents may be part of multiple targets.
- */
- class TargetChange {
- constructor(
- /**
- * An opaque, server-assigned token that allows watching a query to be resumed
- * after disconnecting without retransmitting all the data that matches the
- * query. The resume token essentially identifies a point in time from which
- * the server should resume sending results.
- */
- resumeToken,
- /**
- * The "current" (synced) status of this target. Note that "current"
- * has special meaning in the RPC protocol that implies that a target is
- * both up-to-date and consistent with the rest of the watch stream.
- */
- current,
- /**
- * The set of documents that were newly assigned to this target as part of
- * this remote event.
- */
- addedDocuments,
- /**
- * The set of documents that were already assigned to this target but received
- * an update during this remote event.
- */
- modifiedDocuments,
- /**
- * The set of documents that were removed from this target as part of this
- * remote event.
- */
- removedDocuments) {
- this.resumeToken = resumeToken;
- this.current = current;
- this.addedDocuments = addedDocuments;
- this.modifiedDocuments = modifiedDocuments;
- this.removedDocuments = removedDocuments;
- }
- /**
- * This method is used to create a synthesized TargetChanges that can be used to
- * apply a CURRENT status change to a View (for queries executed in a different
- * tab) or for new queries (to raise snapshots with correct CURRENT status).
- */
- static createSynthesizedTargetChangeForCurrentChange(targetId, current, resumeToken) {
- return new TargetChange(resumeToken, current, documentKeySet(), documentKeySet(), documentKeySet());
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Represents a changed document and a list of target ids to which this change
- * applies.
- *
- * If document has been deleted NoDocument will be provided.
- */
- class DocumentWatchChange {
- constructor(
- /** The new document applies to all of these targets. */
- updatedTargetIds,
- /** The new document is removed from all of these targets. */
- removedTargetIds,
- /** The key of the document for this change. */
- key,
- /**
- * The new document or NoDocument if it was deleted. Is null if the
- * document went out of view without the server sending a new document.
- */
- newDoc) {
- this.updatedTargetIds = updatedTargetIds;
- this.removedTargetIds = removedTargetIds;
- this.key = key;
- this.newDoc = newDoc;
- }
- }
- class ExistenceFilterChange {
- constructor(targetId, existenceFilter) {
- this.targetId = targetId;
- this.existenceFilter = existenceFilter;
- }
- }
- class WatchTargetChange {
- constructor(
- /** What kind of change occurred to the watch target. */
- state,
- /** The target IDs that were added/removed/set. */
- targetIds,
- /**
- * An opaque, server-assigned token that allows watching a target to be
- * resumed after disconnecting without retransmitting all the data that
- * matches the target. The resume token essentially identifies a point in
- * time from which the server should resume sending results.
- */
- resumeToken = ByteString.EMPTY_BYTE_STRING,
- /** An RPC error indicating why the watch failed. */
- cause = null) {
- this.state = state;
- this.targetIds = targetIds;
- this.resumeToken = resumeToken;
- this.cause = cause;
- }
- }
- /** Tracks the internal state of a Watch target. */
- class TargetState {
- constructor() {
- /**
- * The number of pending responses (adds or removes) that we are waiting on.
- * We only consider targets active that have no pending responses.
- */
- this.pendingResponses = 0;
- /**
- * Keeps track of the document changes since the last raised snapshot.
- *
- * These changes are continuously updated as we receive document updates and
- * always reflect the current set of changes against the last issued snapshot.
- */
- this.documentChanges = snapshotChangesMap();
- /** See public getters for explanations of these fields. */
- this._resumeToken = ByteString.EMPTY_BYTE_STRING;
- this._current = false;
- /**
- * Whether this target state should be included in the next snapshot. We
- * initialize to true so that newly-added targets are included in the next
- * RemoteEvent.
- */
- this._hasPendingChanges = true;
- }
- /**
- * Whether this target has been marked 'current'.
- *
- * 'Current' has special meaning in the RPC protocol: It implies that the
- * Watch backend has sent us all changes up to the point at which the target
- * was added and that the target is consistent with the rest of the watch
- * stream.
- */
- get current() {
- return this._current;
- }
- /** The last resume token sent to us for this target. */
- get resumeToken() {
- return this._resumeToken;
- }
- /** Whether this target has pending target adds or target removes. */
- get isPending() {
- return this.pendingResponses !== 0;
- }
- /** Whether we have modified any state that should trigger a snapshot. */
- get hasPendingChanges() {
- return this._hasPendingChanges;
- }
- /**
- * Applies the resume token to the TargetChange, but only when it has a new
- * value. Empty resumeTokens are discarded.
- */
- updateResumeToken(resumeToken) {
- if (resumeToken.approximateByteSize() > 0) {
- this._hasPendingChanges = true;
- this._resumeToken = resumeToken;
- }
- }
- /**
- * Creates a target change from the current set of changes.
- *
- * To reset the document changes after raising this snapshot, call
- * `clearPendingChanges()`.
- */
- toTargetChange() {
- let addedDocuments = documentKeySet();
- let modifiedDocuments = documentKeySet();
- let removedDocuments = documentKeySet();
- this.documentChanges.forEach((key, changeType) => {
- switch (changeType) {
- case 0 /* ChangeType.Added */:
- addedDocuments = addedDocuments.add(key);
- break;
- case 2 /* ChangeType.Modified */:
- modifiedDocuments = modifiedDocuments.add(key);
- break;
- case 1 /* ChangeType.Removed */:
- removedDocuments = removedDocuments.add(key);
- break;
- default:
- fail();
- }
- });
- return new TargetChange(this._resumeToken, this._current, addedDocuments, modifiedDocuments, removedDocuments);
- }
- /**
- * Resets the document changes and sets `hasPendingChanges` to false.
- */
- clearPendingChanges() {
- this._hasPendingChanges = false;
- this.documentChanges = snapshotChangesMap();
- }
- addDocumentChange(key, changeType) {
- this._hasPendingChanges = true;
- this.documentChanges = this.documentChanges.insert(key, changeType);
- }
- removeDocumentChange(key) {
- this._hasPendingChanges = true;
- this.documentChanges = this.documentChanges.remove(key);
- }
- recordPendingTargetRequest() {
- this.pendingResponses += 1;
- }
- recordTargetResponse() {
- this.pendingResponses -= 1;
- }
- markCurrent() {
- this._hasPendingChanges = true;
- this._current = true;
- }
- }
- const LOG_TAG$g = 'WatchChangeAggregator';
- /**
- * A helper class to accumulate watch changes into a RemoteEvent.
- */
- class WatchChangeAggregator {
- constructor(metadataProvider) {
- this.metadataProvider = metadataProvider;
- /** The internal state of all tracked targets. */
- this.targetStates = new Map();
- /** Keeps track of the documents to update since the last raised snapshot. */
- this.pendingDocumentUpdates = mutableDocumentMap();
- /** A mapping of document keys to their set of target IDs. */
- this.pendingDocumentTargetMapping = documentTargetMap();
- /**
- * A map of targets with existence filter mismatches. These targets are
- * known to be inconsistent and their listens needs to be re-established by
- * RemoteStore.
- */
- this.pendingTargetResets = new SortedMap(primitiveComparator);
- }
- /**
- * Processes and adds the DocumentWatchChange to the current set of changes.
- */
- handleDocumentChange(docChange) {
- for (const targetId of docChange.updatedTargetIds) {
- if (docChange.newDoc && docChange.newDoc.isFoundDocument()) {
- this.addDocumentToTarget(targetId, docChange.newDoc);
- }
- else {
- this.removeDocumentFromTarget(targetId, docChange.key, docChange.newDoc);
- }
- }
- for (const targetId of docChange.removedTargetIds) {
- this.removeDocumentFromTarget(targetId, docChange.key, docChange.newDoc);
- }
- }
- /** Processes and adds the WatchTargetChange to the current set of changes. */
- handleTargetChange(targetChange) {
- this.forEachTarget(targetChange, targetId => {
- const targetState = this.ensureTargetState(targetId);
- switch (targetChange.state) {
- case 0 /* WatchTargetChangeState.NoChange */:
- if (this.isActiveTarget(targetId)) {
- targetState.updateResumeToken(targetChange.resumeToken);
- }
- break;
- case 1 /* WatchTargetChangeState.Added */:
- // We need to decrement the number of pending acks needed from watch
- // for this targetId.
- targetState.recordTargetResponse();
- if (!targetState.isPending) {
- // We have a freshly added target, so we need to reset any state
- // that we had previously. This can happen e.g. when remove and add
- // back a target for existence filter mismatches.
- targetState.clearPendingChanges();
- }
- targetState.updateResumeToken(targetChange.resumeToken);
- break;
- case 2 /* WatchTargetChangeState.Removed */:
- // We need to keep track of removed targets to we can post-filter and
- // remove any target changes.
- // We need to decrement the number of pending acks needed from watch
- // for this targetId.
- targetState.recordTargetResponse();
- if (!targetState.isPending) {
- this.removeTarget(targetId);
- }
- break;
- case 3 /* WatchTargetChangeState.Current */:
- if (this.isActiveTarget(targetId)) {
- targetState.markCurrent();
- targetState.updateResumeToken(targetChange.resumeToken);
- }
- break;
- case 4 /* WatchTargetChangeState.Reset */:
- if (this.isActiveTarget(targetId)) {
- // Reset the target and synthesizes removes for all existing
- // documents. The backend will re-add any documents that still
- // match the target before it sends the next global snapshot.
- this.resetTarget(targetId);
- targetState.updateResumeToken(targetChange.resumeToken);
- }
- break;
- default:
- fail();
- }
- });
- }
- /**
- * Iterates over all targetIds that the watch change applies to: either the
- * targetIds explicitly listed in the change or the targetIds of all currently
- * active targets.
- */
- forEachTarget(targetChange, fn) {
- if (targetChange.targetIds.length > 0) {
- targetChange.targetIds.forEach(fn);
- }
- else {
- this.targetStates.forEach((_, targetId) => {
- if (this.isActiveTarget(targetId)) {
- fn(targetId);
- }
- });
- }
- }
- /**
- * Handles existence filters and synthesizes deletes for filter mismatches.
- * Targets that are invalidated by filter mismatches are added to
- * `pendingTargetResets`.
- */
- handleExistenceFilter(watchChange) {
- var _a;
- const targetId = watchChange.targetId;
- const expectedCount = watchChange.existenceFilter.count;
- const targetData = this.targetDataForActiveTarget(targetId);
- if (targetData) {
- const target = targetData.target;
- if (targetIsDocumentTarget(target)) {
- if (expectedCount === 0) {
- // The existence filter told us the document does not exist. We deduce
- // that this document does not exist and apply a deleted document to
- // our updates. Without applying this deleted document there might be
- // another query that will raise this document as part of a snapshot
- // until it is resolved, essentially exposing inconsistency between
- // queries.
- const key = new DocumentKey(target.path);
- this.removeDocumentFromTarget(targetId, key, MutableDocument.newNoDocument(key, SnapshotVersion.min()));
- }
- else {
- hardAssert(expectedCount === 1);
- }
- }
- else {
- const currentSize = this.getCurrentDocumentCountForTarget(targetId);
- // Existence filter mismatch. Mark the documents as being in limbo, and
- // raise a snapshot with `isFromCache:true`.
- if (currentSize !== expectedCount) {
- // Apply bloom filter to identify and mark removed documents.
- const status = this.applyBloomFilter(watchChange, currentSize);
- if (status !== 0 /* BloomFilterApplicationStatus.Success */) {
- // If bloom filter application fails, we reset the mapping and
- // trigger re-run of the query.
- this.resetTarget(targetId);
- const purpose = status === 2 /* BloomFilterApplicationStatus.FalsePositive */
- ? "TargetPurposeExistenceFilterMismatchBloom" /* TargetPurpose.ExistenceFilterMismatchBloom */
- : "TargetPurposeExistenceFilterMismatch" /* TargetPurpose.ExistenceFilterMismatch */;
- this.pendingTargetResets = this.pendingTargetResets.insert(targetId, purpose);
- }
- (_a = TestingHooks.instance) === null || _a === void 0 ? void 0 : _a.notifyOnExistenceFilterMismatch(createExistenceFilterMismatchInfoForTestingHooks(status, currentSize, watchChange.existenceFilter));
- }
- }
- }
- }
- /**
- * Apply bloom filter to remove the deleted documents, and return the
- * application status.
- */
- applyBloomFilter(watchChange, currentCount) {
- const { unchangedNames, count: expectedCount } = watchChange.existenceFilter;
- if (!unchangedNames || !unchangedNames.bits) {
- return 1 /* BloomFilterApplicationStatus.Skipped */;
- }
- const { bits: { bitmap = '', padding = 0 }, hashCount = 0 } = unchangedNames;
- let normalizedBitmap;
- try {
- normalizedBitmap = normalizeByteString(bitmap).toUint8Array();
- }
- catch (err) {
- if (err instanceof Base64DecodeError) {
- logWarn('Decoding the base64 bloom filter in existence filter failed (' +
- err.message +
- '); ignoring the bloom filter and falling back to full re-query.');
- return 1 /* BloomFilterApplicationStatus.Skipped */;
- }
- else {
- throw err;
- }
- }
- let bloomFilter;
- try {
- // BloomFilter throws error if the inputs are invalid.
- bloomFilter = new BloomFilter(normalizedBitmap, padding, hashCount);
- }
- catch (err) {
- if (err instanceof BloomFilterError) {
- logWarn('BloomFilter error: ', err);
- }
- else {
- logWarn('Applying bloom filter failed: ', err);
- }
- return 1 /* BloomFilterApplicationStatus.Skipped */;
- }
- if (bloomFilter.bitCount === 0) {
- return 1 /* BloomFilterApplicationStatus.Skipped */;
- }
- const removedDocumentCount = this.filterRemovedDocuments(watchChange.targetId, bloomFilter);
- if (expectedCount !== currentCount - removedDocumentCount) {
- return 2 /* BloomFilterApplicationStatus.FalsePositive */;
- }
- return 0 /* BloomFilterApplicationStatus.Success */;
- }
- /**
- * Filter out removed documents based on bloom filter membership result and
- * return number of documents removed.
- */
- filterRemovedDocuments(targetId, bloomFilter) {
- const existingKeys = this.metadataProvider.getRemoteKeysForTarget(targetId);
- let removalCount = 0;
- existingKeys.forEach(key => {
- const databaseId = this.metadataProvider.getDatabaseId();
- const documentPath = `projects/${databaseId.projectId}/databases/${databaseId.database}/documents/${key.path.canonicalString()}`;
- if (!bloomFilter.mightContain(documentPath)) {
- this.removeDocumentFromTarget(targetId, key, /*updatedDocument=*/ null);
- removalCount++;
- }
- });
- return removalCount;
- }
- /**
- * Converts the currently accumulated state into a remote event at the
- * provided snapshot version. Resets the accumulated changes before returning.
- */
- createRemoteEvent(snapshotVersion) {
- const targetChanges = new Map();
- this.targetStates.forEach((targetState, targetId) => {
- const targetData = this.targetDataForActiveTarget(targetId);
- if (targetData) {
- if (targetState.current && targetIsDocumentTarget(targetData.target)) {
- // Document queries for document that don't exist can produce an empty
- // result set. To update our local cache, we synthesize a document
- // delete if we have not previously received the document. This
- // resolves the limbo state of the document, removing it from
- // limboDocumentRefs.
- //
- // TODO(dimond): Ideally we would have an explicit lookup target
- // instead resulting in an explicit delete message and we could
- // remove this special logic.
- const key = new DocumentKey(targetData.target.path);
- if (this.pendingDocumentUpdates.get(key) === null &&
- !this.targetContainsDocument(targetId, key)) {
- this.removeDocumentFromTarget(targetId, key, MutableDocument.newNoDocument(key, snapshotVersion));
- }
- }
- if (targetState.hasPendingChanges) {
- targetChanges.set(targetId, targetState.toTargetChange());
- targetState.clearPendingChanges();
- }
- }
- });
- let resolvedLimboDocuments = documentKeySet();
- // We extract the set of limbo-only document updates as the GC logic
- // special-cases documents that do not appear in the target cache.
- //
- // TODO(gsoltis): Expand on this comment once GC is available in the JS
- // client.
- this.pendingDocumentTargetMapping.forEach((key, targets) => {
- let isOnlyLimboTarget = true;
- targets.forEachWhile(targetId => {
- const targetData = this.targetDataForActiveTarget(targetId);
- if (targetData &&
- targetData.purpose !== "TargetPurposeLimboResolution" /* TargetPurpose.LimboResolution */) {
- isOnlyLimboTarget = false;
- return false;
- }
- return true;
- });
- if (isOnlyLimboTarget) {
- resolvedLimboDocuments = resolvedLimboDocuments.add(key);
- }
- });
- this.pendingDocumentUpdates.forEach((_, doc) => doc.setReadTime(snapshotVersion));
- const remoteEvent = new RemoteEvent(snapshotVersion, targetChanges, this.pendingTargetResets, this.pendingDocumentUpdates, resolvedLimboDocuments);
- this.pendingDocumentUpdates = mutableDocumentMap();
- this.pendingDocumentTargetMapping = documentTargetMap();
- this.pendingTargetResets = new SortedMap(primitiveComparator);
- return remoteEvent;
- }
- /**
- * Adds the provided document to the internal list of document updates and
- * its document key to the given target's mapping.
- */
- // Visible for testing.
- addDocumentToTarget(targetId, document) {
- if (!this.isActiveTarget(targetId)) {
- return;
- }
- const changeType = this.targetContainsDocument(targetId, document.key)
- ? 2 /* ChangeType.Modified */
- : 0 /* ChangeType.Added */;
- const targetState = this.ensureTargetState(targetId);
- targetState.addDocumentChange(document.key, changeType);
- this.pendingDocumentUpdates = this.pendingDocumentUpdates.insert(document.key, document);
- this.pendingDocumentTargetMapping =
- this.pendingDocumentTargetMapping.insert(document.key, this.ensureDocumentTargetMapping(document.key).add(targetId));
- }
- /**
- * Removes the provided document from the target mapping. If the
- * document no longer matches the target, but the document's state is still
- * known (e.g. we know that the document was deleted or we received the change
- * that caused the filter mismatch), the new document can be provided
- * to update the remote document cache.
- */
- // Visible for testing.
- removeDocumentFromTarget(targetId, key, updatedDocument) {
- if (!this.isActiveTarget(targetId)) {
- return;
- }
- const targetState = this.ensureTargetState(targetId);
- if (this.targetContainsDocument(targetId, key)) {
- targetState.addDocumentChange(key, 1 /* ChangeType.Removed */);
- }
- else {
- // The document may have entered and left the target before we raised a
- // snapshot, so we can just ignore the change.
- targetState.removeDocumentChange(key);
- }
- this.pendingDocumentTargetMapping =
- this.pendingDocumentTargetMapping.insert(key, this.ensureDocumentTargetMapping(key).delete(targetId));
- if (updatedDocument) {
- this.pendingDocumentUpdates = this.pendingDocumentUpdates.insert(key, updatedDocument);
- }
- }
- removeTarget(targetId) {
- this.targetStates.delete(targetId);
- }
- /**
- * Returns the current count of documents in the target. This includes both
- * the number of documents that the LocalStore considers to be part of the
- * target as well as any accumulated changes.
- */
- getCurrentDocumentCountForTarget(targetId) {
- const targetState = this.ensureTargetState(targetId);
- const targetChange = targetState.toTargetChange();
- return (this.metadataProvider.getRemoteKeysForTarget(targetId).size +
- targetChange.addedDocuments.size -
- targetChange.removedDocuments.size);
- }
- /**
- * Increment the number of acks needed from watch before we can consider the
- * server to be 'in-sync' with the client's active targets.
- */
- recordPendingTargetRequest(targetId) {
- // For each request we get we need to record we need a response for it.
- const targetState = this.ensureTargetState(targetId);
- targetState.recordPendingTargetRequest();
- }
- ensureTargetState(targetId) {
- let result = this.targetStates.get(targetId);
- if (!result) {
- result = new TargetState();
- this.targetStates.set(targetId, result);
- }
- return result;
- }
- ensureDocumentTargetMapping(key) {
- let targetMapping = this.pendingDocumentTargetMapping.get(key);
- if (!targetMapping) {
- targetMapping = new SortedSet(primitiveComparator);
- this.pendingDocumentTargetMapping =
- this.pendingDocumentTargetMapping.insert(key, targetMapping);
- }
- return targetMapping;
- }
- /**
- * Verifies that the user is still interested in this target (by calling
- * `getTargetDataForTarget()`) and that we are not waiting for pending ADDs
- * from watch.
- */
- isActiveTarget(targetId) {
- const targetActive = this.targetDataForActiveTarget(targetId) !== null;
- if (!targetActive) {
- logDebug(LOG_TAG$g, 'Detected inactive target', targetId);
- }
- return targetActive;
- }
- /**
- * Returns the TargetData for an active target (i.e. a target that the user
- * is still interested in that has no outstanding target change requests).
- */
- targetDataForActiveTarget(targetId) {
- const targetState = this.targetStates.get(targetId);
- return targetState && targetState.isPending
- ? null
- : this.metadataProvider.getTargetDataForTarget(targetId);
- }
- /**
- * Resets the state of a Watch target to its initial state (e.g. sets
- * 'current' to false, clears the resume token and removes its target mapping
- * from all documents).
- */
- resetTarget(targetId) {
- this.targetStates.set(targetId, new TargetState());
- // Trigger removal for any documents currently mapped to this target.
- // These removals will be part of the initial snapshot if Watch does not
- // resend these documents.
- const existingKeys = this.metadataProvider.getRemoteKeysForTarget(targetId);
- existingKeys.forEach(key => {
- this.removeDocumentFromTarget(targetId, key, /*updatedDocument=*/ null);
- });
- }
- /**
- * Returns whether the LocalStore considers the document to be part of the
- * specified target.
- */
- targetContainsDocument(targetId, key) {
- const existingKeys = this.metadataProvider.getRemoteKeysForTarget(targetId);
- return existingKeys.has(key);
- }
- }
- function documentTargetMap() {
- return new SortedMap(DocumentKey.comparator);
- }
- function snapshotChangesMap() {
- return new SortedMap(DocumentKey.comparator);
- }
- function createExistenceFilterMismatchInfoForTestingHooks(status, localCacheCount, existenceFilter) {
- var _a, _b, _c, _d, _e, _f;
- const result = {
- localCacheCount,
- existenceFilterCount: existenceFilter.count
- };
- const unchangedNames = existenceFilter.unchangedNames;
- if (unchangedNames) {
- result.bloomFilter = {
- applied: status === 0 /* BloomFilterApplicationStatus.Success */,
- hashCount: (_a = unchangedNames === null || unchangedNames === void 0 ? void 0 : unchangedNames.hashCount) !== null && _a !== void 0 ? _a : 0,
- bitmapLength: (_d = (_c = (_b = unchangedNames === null || unchangedNames === void 0 ? void 0 : unchangedNames.bits) === null || _b === void 0 ? void 0 : _b.bitmap) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0,
- padding: (_f = (_e = unchangedNames === null || unchangedNames === void 0 ? void 0 : unchangedNames.bits) === null || _e === void 0 ? void 0 : _e.padding) !== null && _f !== void 0 ? _f : 0
- };
- }
- return result;
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const DIRECTIONS = (() => {
- const dirs = {};
- dirs["asc" /* Direction.ASCENDING */] = 'ASCENDING';
- dirs["desc" /* Direction.DESCENDING */] = 'DESCENDING';
- return dirs;
- })();
- const OPERATORS = (() => {
- const ops = {};
- ops["<" /* Operator.LESS_THAN */] = 'LESS_THAN';
- ops["<=" /* Operator.LESS_THAN_OR_EQUAL */] = 'LESS_THAN_OR_EQUAL';
- ops[">" /* Operator.GREATER_THAN */] = 'GREATER_THAN';
- ops[">=" /* Operator.GREATER_THAN_OR_EQUAL */] = 'GREATER_THAN_OR_EQUAL';
- ops["==" /* Operator.EQUAL */] = 'EQUAL';
- ops["!=" /* Operator.NOT_EQUAL */] = 'NOT_EQUAL';
- ops["array-contains" /* Operator.ARRAY_CONTAINS */] = 'ARRAY_CONTAINS';
- ops["in" /* Operator.IN */] = 'IN';
- ops["not-in" /* Operator.NOT_IN */] = 'NOT_IN';
- ops["array-contains-any" /* Operator.ARRAY_CONTAINS_ANY */] = 'ARRAY_CONTAINS_ANY';
- return ops;
- })();
- const COMPOSITE_OPERATORS = (() => {
- const ops = {};
- ops["and" /* CompositeOperator.AND */] = 'AND';
- ops["or" /* CompositeOperator.OR */] = 'OR';
- return ops;
- })();
- function assertPresent(value, description) {
- }
- /**
- * This class generates JsonObject values for the Datastore API suitable for
- * sending to either GRPC stub methods or via the JSON/HTTP REST API.
- *
- * The serializer supports both Protobuf.js and Proto3 JSON formats. By
- * setting `useProto3Json` to true, the serializer will use the Proto3 JSON
- * format.
- *
- * For a description of the Proto3 JSON format check
- * https://developers.google.com/protocol-buffers/docs/proto3#json
- *
- * TODO(klimt): We can remove the databaseId argument if we keep the full
- * resource name in documents.
- */
- class JsonProtoSerializer {
- constructor(databaseId, useProto3Json) {
- this.databaseId = databaseId;
- this.useProto3Json = useProto3Json;
- }
- }
- function fromRpcStatus(status) {
- const code = status.code === undefined ? Code.UNKNOWN : mapCodeFromRpcCode(status.code);
- return new FirestoreError(code, status.message || '');
- }
- /**
- * Returns a value for a number (or null) that's appropriate to put into
- * a google.protobuf.Int32Value proto.
- * DO NOT USE THIS FOR ANYTHING ELSE.
- * This method cheats. It's typed as returning "number" because that's what
- * our generated proto interfaces say Int32Value must be. But GRPC actually
- * expects a { value: <number> } struct.
- */
- function toInt32Proto(serializer, val) {
- if (serializer.useProto3Json || isNullOrUndefined(val)) {
- return val;
- }
- else {
- return { value: val };
- }
- }
- /**
- * Returns a number (or null) from a google.protobuf.Int32Value proto.
- */
- function fromInt32Proto(val) {
- let result;
- if (typeof val === 'object') {
- result = val.value;
- }
- else {
- result = val;
- }
- return isNullOrUndefined(result) ? null : result;
- }
- /**
- * Returns a value for a Date that's appropriate to put into a proto.
- */
- function toTimestamp(serializer, timestamp) {
- if (serializer.useProto3Json) {
- // Serialize to ISO-8601 date format, but with full nano resolution.
- // Since JS Date has only millis, let's only use it for the seconds and
- // then manually add the fractions to the end.
- const jsDateStr = new Date(timestamp.seconds * 1000).toISOString();
- // Remove .xxx frac part and Z in the end.
- const strUntilSeconds = jsDateStr.replace(/\.\d*/, '').replace('Z', '');
- // Pad the fraction out to 9 digits (nanos).
- const nanoStr = ('000000000' + timestamp.nanoseconds).slice(-9);
- return `${strUntilSeconds}.${nanoStr}Z`;
- }
- else {
- return {
- seconds: '' + timestamp.seconds,
- nanos: timestamp.nanoseconds
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- };
- }
- }
- function fromTimestamp(date) {
- const timestamp = normalizeTimestamp(date);
- return new Timestamp(timestamp.seconds, timestamp.nanos);
- }
- /**
- * Returns a value for bytes that's appropriate to put in a proto.
- *
- * Visible for testing.
- */
- function toBytes(serializer, bytes) {
- if (serializer.useProto3Json) {
- return bytes.toBase64();
- }
- else {
- return bytes.toUint8Array();
- }
- }
- /**
- * Returns a ByteString based on the proto string value.
- */
- function fromBytes(serializer, value) {
- if (serializer.useProto3Json) {
- hardAssert(value === undefined || typeof value === 'string');
- return ByteString.fromBase64String(value ? value : '');
- }
- else {
- hardAssert(value === undefined || value instanceof Uint8Array);
- return ByteString.fromUint8Array(value ? value : new Uint8Array());
- }
- }
- function toVersion(serializer, version) {
- return toTimestamp(serializer, version.toTimestamp());
- }
- function fromVersion(version) {
- hardAssert(!!version);
- return SnapshotVersion.fromTimestamp(fromTimestamp(version));
- }
- function toResourceName(databaseId, path) {
- return fullyQualifiedPrefixPath(databaseId)
- .child('documents')
- .child(path)
- .canonicalString();
- }
- function fromResourceName(name) {
- const resource = ResourcePath.fromString(name);
- hardAssert(isValidResourceName(resource));
- return resource;
- }
- function toName(serializer, key) {
- return toResourceName(serializer.databaseId, key.path);
- }
- function fromName(serializer, name) {
- const resource = fromResourceName(name);
- if (resource.get(1) !== serializer.databaseId.projectId) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Tried to deserialize key from different project: ' +
- resource.get(1) +
- ' vs ' +
- serializer.databaseId.projectId);
- }
- if (resource.get(3) !== serializer.databaseId.database) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Tried to deserialize key from different database: ' +
- resource.get(3) +
- ' vs ' +
- serializer.databaseId.database);
- }
- return new DocumentKey(extractLocalPathFromResourceName(resource));
- }
- function toQueryPath(serializer, path) {
- return toResourceName(serializer.databaseId, path);
- }
- function fromQueryPath(name) {
- const resourceName = fromResourceName(name);
- // In v1beta1 queries for collections at the root did not have a trailing
- // "/documents". In v1 all resource paths contain "/documents". Preserve the
- // ability to read the v1beta1 form for compatibility with queries persisted
- // in the local target cache.
- if (resourceName.length === 4) {
- return ResourcePath.emptyPath();
- }
- return extractLocalPathFromResourceName(resourceName);
- }
- function getEncodedDatabaseId(serializer) {
- const path = new ResourcePath([
- 'projects',
- serializer.databaseId.projectId,
- 'databases',
- serializer.databaseId.database
- ]);
- return path.canonicalString();
- }
- function fullyQualifiedPrefixPath(databaseId) {
- return new ResourcePath([
- 'projects',
- databaseId.projectId,
- 'databases',
- databaseId.database
- ]);
- }
- function extractLocalPathFromResourceName(resourceName) {
- hardAssert(resourceName.length > 4 && resourceName.get(4) === 'documents');
- return resourceName.popFirst(5);
- }
- /** Creates a Document proto from key and fields (but no create/update time) */
- function toMutationDocument(serializer, key, fields) {
- return {
- name: toName(serializer, key),
- fields: fields.value.mapValue.fields
- };
- }
- function toDocument(serializer, document) {
- return {
- name: toName(serializer, document.key),
- fields: document.data.value.mapValue.fields,
- updateTime: toTimestamp(serializer, document.version.toTimestamp()),
- createTime: toTimestamp(serializer, document.createTime.toTimestamp())
- };
- }
- function fromDocument(serializer, document, hasCommittedMutations) {
- const key = fromName(serializer, document.name);
- const version = fromVersion(document.updateTime);
- // If we read a document from persistence that is missing createTime, it's due
- // to older SDK versions not storing this information. In such cases, we'll
- // set the createTime to zero. This can be removed in the long term.
- const createTime = document.createTime
- ? fromVersion(document.createTime)
- : SnapshotVersion.min();
- const data = new ObjectValue({ mapValue: { fields: document.fields } });
- const result = MutableDocument.newFoundDocument(key, version, createTime, data);
- if (hasCommittedMutations) {
- result.setHasCommittedMutations();
- }
- return hasCommittedMutations ? result.setHasCommittedMutations() : result;
- }
- function fromFound(serializer, doc) {
- hardAssert(!!doc.found);
- assertPresent(doc.found.name);
- assertPresent(doc.found.updateTime);
- const key = fromName(serializer, doc.found.name);
- const version = fromVersion(doc.found.updateTime);
- const createTime = doc.found.createTime
- ? fromVersion(doc.found.createTime)
- : SnapshotVersion.min();
- const data = new ObjectValue({ mapValue: { fields: doc.found.fields } });
- return MutableDocument.newFoundDocument(key, version, createTime, data);
- }
- function fromMissing(serializer, result) {
- hardAssert(!!result.missing);
- hardAssert(!!result.readTime);
- const key = fromName(serializer, result.missing);
- const version = fromVersion(result.readTime);
- return MutableDocument.newNoDocument(key, version);
- }
- function fromBatchGetDocumentsResponse(serializer, result) {
- if ('found' in result) {
- return fromFound(serializer, result);
- }
- else if ('missing' in result) {
- return fromMissing(serializer, result);
- }
- return fail();
- }
- function fromWatchChange(serializer, change) {
- let watchChange;
- if ('targetChange' in change) {
- assertPresent(change.targetChange);
- // proto3 default value is unset in JSON (undefined), so use 'NO_CHANGE'
- // if unset
- const state = fromWatchTargetChangeState(change.targetChange.targetChangeType || 'NO_CHANGE');
- const targetIds = change.targetChange.targetIds || [];
- const resumeToken = fromBytes(serializer, change.targetChange.resumeToken);
- const causeProto = change.targetChange.cause;
- const cause = causeProto && fromRpcStatus(causeProto);
- watchChange = new WatchTargetChange(state, targetIds, resumeToken, cause || null);
- }
- else if ('documentChange' in change) {
- assertPresent(change.documentChange);
- const entityChange = change.documentChange;
- assertPresent(entityChange.document);
- assertPresent(entityChange.document.name);
- assertPresent(entityChange.document.updateTime);
- const key = fromName(serializer, entityChange.document.name);
- const version = fromVersion(entityChange.document.updateTime);
- const createTime = entityChange.document.createTime
- ? fromVersion(entityChange.document.createTime)
- : SnapshotVersion.min();
- const data = new ObjectValue({
- mapValue: { fields: entityChange.document.fields }
- });
- const doc = MutableDocument.newFoundDocument(key, version, createTime, data);
- const updatedTargetIds = entityChange.targetIds || [];
- const removedTargetIds = entityChange.removedTargetIds || [];
- watchChange = new DocumentWatchChange(updatedTargetIds, removedTargetIds, doc.key, doc);
- }
- else if ('documentDelete' in change) {
- assertPresent(change.documentDelete);
- const docDelete = change.documentDelete;
- assertPresent(docDelete.document);
- const key = fromName(serializer, docDelete.document);
- const version = docDelete.readTime
- ? fromVersion(docDelete.readTime)
- : SnapshotVersion.min();
- const doc = MutableDocument.newNoDocument(key, version);
- const removedTargetIds = docDelete.removedTargetIds || [];
- watchChange = new DocumentWatchChange([], removedTargetIds, doc.key, doc);
- }
- else if ('documentRemove' in change) {
- assertPresent(change.documentRemove);
- const docRemove = change.documentRemove;
- assertPresent(docRemove.document);
- const key = fromName(serializer, docRemove.document);
- const removedTargetIds = docRemove.removedTargetIds || [];
- watchChange = new DocumentWatchChange([], removedTargetIds, key, null);
- }
- else if ('filter' in change) {
- // TODO(dimond): implement existence filter parsing with strategy.
- assertPresent(change.filter);
- const filter = change.filter;
- assertPresent(filter.targetId);
- const { count = 0, unchangedNames } = filter;
- const existenceFilter = new ExistenceFilter(count, unchangedNames);
- const targetId = filter.targetId;
- watchChange = new ExistenceFilterChange(targetId, existenceFilter);
- }
- else {
- return fail();
- }
- return watchChange;
- }
- function fromWatchTargetChangeState(state) {
- if (state === 'NO_CHANGE') {
- return 0 /* WatchTargetChangeState.NoChange */;
- }
- else if (state === 'ADD') {
- return 1 /* WatchTargetChangeState.Added */;
- }
- else if (state === 'REMOVE') {
- return 2 /* WatchTargetChangeState.Removed */;
- }
- else if (state === 'CURRENT') {
- return 3 /* WatchTargetChangeState.Current */;
- }
- else if (state === 'RESET') {
- return 4 /* WatchTargetChangeState.Reset */;
- }
- else {
- return fail();
- }
- }
- function versionFromListenResponse(change) {
- // We have only reached a consistent snapshot for the entire stream if there
- // is a read_time set and it applies to all targets (i.e. the list of
- // targets is empty). The backend is guaranteed to send such responses.
- if (!('targetChange' in change)) {
- return SnapshotVersion.min();
- }
- const targetChange = change.targetChange;
- if (targetChange.targetIds && targetChange.targetIds.length) {
- return SnapshotVersion.min();
- }
- if (!targetChange.readTime) {
- return SnapshotVersion.min();
- }
- return fromVersion(targetChange.readTime);
- }
- function toMutation(serializer, mutation) {
- let result;
- if (mutation instanceof SetMutation) {
- result = {
- update: toMutationDocument(serializer, mutation.key, mutation.value)
- };
- }
- else if (mutation instanceof DeleteMutation) {
- result = { delete: toName(serializer, mutation.key) };
- }
- else if (mutation instanceof PatchMutation) {
- result = {
- update: toMutationDocument(serializer, mutation.key, mutation.data),
- updateMask: toDocumentMask(mutation.fieldMask)
- };
- }
- else if (mutation instanceof VerifyMutation) {
- result = {
- verify: toName(serializer, mutation.key)
- };
- }
- else {
- return fail();
- }
- if (mutation.fieldTransforms.length > 0) {
- result.updateTransforms = mutation.fieldTransforms.map(transform => toFieldTransform(serializer, transform));
- }
- if (!mutation.precondition.isNone) {
- result.currentDocument = toPrecondition(serializer, mutation.precondition);
- }
- return result;
- }
- function fromMutation(serializer, proto) {
- const precondition = proto.currentDocument
- ? fromPrecondition(proto.currentDocument)
- : Precondition.none();
- const fieldTransforms = proto.updateTransforms
- ? proto.updateTransforms.map(transform => fromFieldTransform(serializer, transform))
- : [];
- if (proto.update) {
- assertPresent(proto.update.name);
- const key = fromName(serializer, proto.update.name);
- const value = new ObjectValue({
- mapValue: { fields: proto.update.fields }
- });
- if (proto.updateMask) {
- const fieldMask = fromDocumentMask(proto.updateMask);
- return new PatchMutation(key, value, fieldMask, precondition, fieldTransforms);
- }
- else {
- return new SetMutation(key, value, precondition, fieldTransforms);
- }
- }
- else if (proto.delete) {
- const key = fromName(serializer, proto.delete);
- return new DeleteMutation(key, precondition);
- }
- else if (proto.verify) {
- const key = fromName(serializer, proto.verify);
- return new VerifyMutation(key, precondition);
- }
- else {
- return fail();
- }
- }
- function toPrecondition(serializer, precondition) {
- if (precondition.updateTime !== undefined) {
- return {
- updateTime: toVersion(serializer, precondition.updateTime)
- };
- }
- else if (precondition.exists !== undefined) {
- return { exists: precondition.exists };
- }
- else {
- return fail();
- }
- }
- function fromPrecondition(precondition) {
- if (precondition.updateTime !== undefined) {
- return Precondition.updateTime(fromVersion(precondition.updateTime));
- }
- else if (precondition.exists !== undefined) {
- return Precondition.exists(precondition.exists);
- }
- else {
- return Precondition.none();
- }
- }
- function fromWriteResult(proto, commitTime) {
- // NOTE: Deletes don't have an updateTime.
- let version = proto.updateTime
- ? fromVersion(proto.updateTime)
- : fromVersion(commitTime);
- if (version.isEqual(SnapshotVersion.min())) {
- // The Firestore Emulator currently returns an update time of 0 for
- // deletes of non-existing documents (rather than null). This breaks the
- // test "get deleted doc while offline with source=cache" as NoDocuments
- // with version 0 are filtered by IndexedDb's RemoteDocumentCache.
- // TODO(#2149): Remove this when Emulator is fixed
- version = fromVersion(commitTime);
- }
- return new MutationResult(version, proto.transformResults || []);
- }
- function fromWriteResults(protos, commitTime) {
- if (protos && protos.length > 0) {
- hardAssert(commitTime !== undefined);
- return protos.map(proto => fromWriteResult(proto, commitTime));
- }
- else {
- return [];
- }
- }
- function toFieldTransform(serializer, fieldTransform) {
- const transform = fieldTransform.transform;
- if (transform instanceof ServerTimestampTransform) {
- return {
- fieldPath: fieldTransform.field.canonicalString(),
- setToServerValue: 'REQUEST_TIME'
- };
- }
- else if (transform instanceof ArrayUnionTransformOperation) {
- return {
- fieldPath: fieldTransform.field.canonicalString(),
- appendMissingElements: {
- values: transform.elements
- }
- };
- }
- else if (transform instanceof ArrayRemoveTransformOperation) {
- return {
- fieldPath: fieldTransform.field.canonicalString(),
- removeAllFromArray: {
- values: transform.elements
- }
- };
- }
- else if (transform instanceof NumericIncrementTransformOperation) {
- return {
- fieldPath: fieldTransform.field.canonicalString(),
- increment: transform.operand
- };
- }
- else {
- throw fail();
- }
- }
- function fromFieldTransform(serializer, proto) {
- let transform = null;
- if ('setToServerValue' in proto) {
- hardAssert(proto.setToServerValue === 'REQUEST_TIME');
- transform = new ServerTimestampTransform();
- }
- else if ('appendMissingElements' in proto) {
- const values = proto.appendMissingElements.values || [];
- transform = new ArrayUnionTransformOperation(values);
- }
- else if ('removeAllFromArray' in proto) {
- const values = proto.removeAllFromArray.values || [];
- transform = new ArrayRemoveTransformOperation(values);
- }
- else if ('increment' in proto) {
- transform = new NumericIncrementTransformOperation(serializer, proto.increment);
- }
- else {
- fail();
- }
- const fieldPath = FieldPath$1.fromServerFormat(proto.fieldPath);
- return new FieldTransform(fieldPath, transform);
- }
- function toDocumentsTarget(serializer, target) {
- return { documents: [toQueryPath(serializer, target.path)] };
- }
- function fromDocumentsTarget(documentsTarget) {
- const count = documentsTarget.documents.length;
- hardAssert(count === 1);
- const name = documentsTarget.documents[0];
- return queryToTarget(newQueryForPath(fromQueryPath(name)));
- }
- function toQueryTarget(serializer, target) {
- // Dissect the path into parent, collectionId, and optional key filter.
- const result = { structuredQuery: {} };
- const path = target.path;
- if (target.collectionGroup !== null) {
- result.parent = toQueryPath(serializer, path);
- result.structuredQuery.from = [
- {
- collectionId: target.collectionGroup,
- allDescendants: true
- }
- ];
- }
- else {
- result.parent = toQueryPath(serializer, path.popLast());
- result.structuredQuery.from = [{ collectionId: path.lastSegment() }];
- }
- const where = toFilters(target.filters);
- if (where) {
- result.structuredQuery.where = where;
- }
- const orderBy = toOrder(target.orderBy);
- if (orderBy) {
- result.structuredQuery.orderBy = orderBy;
- }
- const limit = toInt32Proto(serializer, target.limit);
- if (limit !== null) {
- result.structuredQuery.limit = limit;
- }
- if (target.startAt) {
- result.structuredQuery.startAt = toStartAtCursor(target.startAt);
- }
- if (target.endAt) {
- result.structuredQuery.endAt = toEndAtCursor(target.endAt);
- }
- return result;
- }
- function toRunAggregationQueryRequest(serializer, target, aggregates) {
- const queryTarget = toQueryTarget(serializer, target);
- const aliasMap = {};
- const aggregations = [];
- let aggregationNum = 0;
- aggregates.forEach(aggregate => {
- // Map all client-side aliases to a unique short-form
- // alias. This avoids issues with client-side aliases that
- // exceed the 1500-byte string size limit.
- const serverAlias = `aggregate_${aggregationNum++}`;
- aliasMap[serverAlias] = aggregate.alias;
- if (aggregate.aggregateType === 'count') {
- aggregations.push({
- alias: serverAlias,
- count: {}
- });
- }
- else if (aggregate.aggregateType === 'avg') {
- aggregations.push({
- alias: serverAlias,
- avg: {
- field: toFieldPathReference(aggregate.fieldPath)
- }
- });
- }
- else if (aggregate.aggregateType === 'sum') {
- aggregations.push({
- alias: serverAlias,
- sum: {
- field: toFieldPathReference(aggregate.fieldPath)
- }
- });
- }
- });
- return {
- request: {
- structuredAggregationQuery: {
- aggregations,
- structuredQuery: queryTarget.structuredQuery
- },
- parent: queryTarget.parent
- },
- aliasMap
- };
- }
- function convertQueryTargetToQuery(target) {
- let path = fromQueryPath(target.parent);
- const query = target.structuredQuery;
- const fromCount = query.from ? query.from.length : 0;
- let collectionGroup = null;
- if (fromCount > 0) {
- hardAssert(fromCount === 1);
- const from = query.from[0];
- if (from.allDescendants) {
- collectionGroup = from.collectionId;
- }
- else {
- path = path.child(from.collectionId);
- }
- }
- let filterBy = [];
- if (query.where) {
- filterBy = fromFilters(query.where);
- }
- let orderBy = [];
- if (query.orderBy) {
- orderBy = fromOrder(query.orderBy);
- }
- let limit = null;
- if (query.limit) {
- limit = fromInt32Proto(query.limit);
- }
- let startAt = null;
- if (query.startAt) {
- startAt = fromStartAtCursor(query.startAt);
- }
- let endAt = null;
- if (query.endAt) {
- endAt = fromEndAtCursor(query.endAt);
- }
- return newQuery(path, collectionGroup, orderBy, filterBy, limit, "F" /* LimitType.First */, startAt, endAt);
- }
- function fromQueryTarget(target) {
- return queryToTarget(convertQueryTargetToQuery(target));
- }
- function toListenRequestLabels(serializer, targetData) {
- const value = toLabel(targetData.purpose);
- if (value == null) {
- return null;
- }
- else {
- return {
- 'goog-listen-tags': value
- };
- }
- }
- function toLabel(purpose) {
- switch (purpose) {
- case "TargetPurposeListen" /* TargetPurpose.Listen */:
- return null;
- case "TargetPurposeExistenceFilterMismatch" /* TargetPurpose.ExistenceFilterMismatch */:
- return 'existence-filter-mismatch';
- case "TargetPurposeExistenceFilterMismatchBloom" /* TargetPurpose.ExistenceFilterMismatchBloom */:
- return 'existence-filter-mismatch-bloom';
- case "TargetPurposeLimboResolution" /* TargetPurpose.LimboResolution */:
- return 'limbo-document';
- default:
- return fail();
- }
- }
- function toTarget(serializer, targetData) {
- let result;
- const target = targetData.target;
- if (targetIsDocumentTarget(target)) {
- result = { documents: toDocumentsTarget(serializer, target) };
- }
- else {
- result = { query: toQueryTarget(serializer, target) };
- }
- result.targetId = targetData.targetId;
- if (targetData.resumeToken.approximateByteSize() > 0) {
- result.resumeToken = toBytes(serializer, targetData.resumeToken);
- const expectedCount = toInt32Proto(serializer, targetData.expectedCount);
- if (expectedCount !== null) {
- result.expectedCount = expectedCount;
- }
- }
- else if (targetData.snapshotVersion.compareTo(SnapshotVersion.min()) > 0) {
- // TODO(wuandy): Consider removing above check because it is most likely true.
- // Right now, many tests depend on this behaviour though (leaving min() out
- // of serialization).
- result.readTime = toTimestamp(serializer, targetData.snapshotVersion.toTimestamp());
- const expectedCount = toInt32Proto(serializer, targetData.expectedCount);
- if (expectedCount !== null) {
- result.expectedCount = expectedCount;
- }
- }
- return result;
- }
- function toFilters(filters) {
- if (filters.length === 0) {
- return;
- }
- return toFilter(CompositeFilter.create(filters, "and" /* CompositeOperator.AND */));
- }
- function fromFilters(filter) {
- const result = fromFilter(filter);
- if (result instanceof CompositeFilter &&
- compositeFilterIsFlatConjunction(result)) {
- return result.getFilters();
- }
- return [result];
- }
- function fromFilter(filter) {
- if (filter.unaryFilter !== undefined) {
- return fromUnaryFilter(filter);
- }
- else if (filter.fieldFilter !== undefined) {
- return fromFieldFilter(filter);
- }
- else if (filter.compositeFilter !== undefined) {
- return fromCompositeFilter(filter);
- }
- else {
- return fail();
- }
- }
- function toOrder(orderBys) {
- if (orderBys.length === 0) {
- return;
- }
- return orderBys.map(order => toPropertyOrder(order));
- }
- function fromOrder(orderBys) {
- return orderBys.map(order => fromPropertyOrder(order));
- }
- function toStartAtCursor(cursor) {
- return {
- before: cursor.inclusive,
- values: cursor.position
- };
- }
- function toEndAtCursor(cursor) {
- return {
- before: !cursor.inclusive,
- values: cursor.position
- };
- }
- function fromStartAtCursor(cursor) {
- const inclusive = !!cursor.before;
- const position = cursor.values || [];
- return new Bound(position, inclusive);
- }
- function fromEndAtCursor(cursor) {
- const inclusive = !cursor.before;
- const position = cursor.values || [];
- return new Bound(position, inclusive);
- }
- // visible for testing
- function toDirection(dir) {
- return DIRECTIONS[dir];
- }
- // visible for testing
- function fromDirection(dir) {
- switch (dir) {
- case 'ASCENDING':
- return "asc" /* Direction.ASCENDING */;
- case 'DESCENDING':
- return "desc" /* Direction.DESCENDING */;
- default:
- return undefined;
- }
- }
- // visible for testing
- function toOperatorName(op) {
- return OPERATORS[op];
- }
- function toCompositeOperatorName(op) {
- return COMPOSITE_OPERATORS[op];
- }
- function fromOperatorName(op) {
- switch (op) {
- case 'EQUAL':
- return "==" /* Operator.EQUAL */;
- case 'NOT_EQUAL':
- return "!=" /* Operator.NOT_EQUAL */;
- case 'GREATER_THAN':
- return ">" /* Operator.GREATER_THAN */;
- case 'GREATER_THAN_OR_EQUAL':
- return ">=" /* Operator.GREATER_THAN_OR_EQUAL */;
- case 'LESS_THAN':
- return "<" /* Operator.LESS_THAN */;
- case 'LESS_THAN_OR_EQUAL':
- return "<=" /* Operator.LESS_THAN_OR_EQUAL */;
- case 'ARRAY_CONTAINS':
- return "array-contains" /* Operator.ARRAY_CONTAINS */;
- case 'IN':
- return "in" /* Operator.IN */;
- case 'NOT_IN':
- return "not-in" /* Operator.NOT_IN */;
- case 'ARRAY_CONTAINS_ANY':
- return "array-contains-any" /* Operator.ARRAY_CONTAINS_ANY */;
- case 'OPERATOR_UNSPECIFIED':
- return fail();
- default:
- return fail();
- }
- }
- function fromCompositeOperatorName(op) {
- switch (op) {
- case 'AND':
- return "and" /* CompositeOperator.AND */;
- case 'OR':
- return "or" /* CompositeOperator.OR */;
- default:
- return fail();
- }
- }
- function toFieldPathReference(path) {
- return { fieldPath: path.canonicalString() };
- }
- function fromFieldPathReference(fieldReference) {
- return FieldPath$1.fromServerFormat(fieldReference.fieldPath);
- }
- // visible for testing
- function toPropertyOrder(orderBy) {
- return {
- field: toFieldPathReference(orderBy.field),
- direction: toDirection(orderBy.dir)
- };
- }
- function fromPropertyOrder(orderBy) {
- return new OrderBy(fromFieldPathReference(orderBy.field), fromDirection(orderBy.direction));
- }
- // visible for testing
- function toFilter(filter) {
- if (filter instanceof FieldFilter) {
- return toUnaryOrFieldFilter(filter);
- }
- else if (filter instanceof CompositeFilter) {
- return toCompositeFilter(filter);
- }
- else {
- return fail();
- }
- }
- function toCompositeFilter(filter) {
- const protos = filter.getFilters().map(filter => toFilter(filter));
- if (protos.length === 1) {
- return protos[0];
- }
- return {
- compositeFilter: {
- op: toCompositeOperatorName(filter.op),
- filters: protos
- }
- };
- }
- function toUnaryOrFieldFilter(filter) {
- if (filter.op === "==" /* Operator.EQUAL */) {
- if (isNanValue(filter.value)) {
- return {
- unaryFilter: {
- field: toFieldPathReference(filter.field),
- op: 'IS_NAN'
- }
- };
- }
- else if (isNullValue(filter.value)) {
- return {
- unaryFilter: {
- field: toFieldPathReference(filter.field),
- op: 'IS_NULL'
- }
- };
- }
- }
- else if (filter.op === "!=" /* Operator.NOT_EQUAL */) {
- if (isNanValue(filter.value)) {
- return {
- unaryFilter: {
- field: toFieldPathReference(filter.field),
- op: 'IS_NOT_NAN'
- }
- };
- }
- else if (isNullValue(filter.value)) {
- return {
- unaryFilter: {
- field: toFieldPathReference(filter.field),
- op: 'IS_NOT_NULL'
- }
- };
- }
- }
- return {
- fieldFilter: {
- field: toFieldPathReference(filter.field),
- op: toOperatorName(filter.op),
- value: filter.value
- }
- };
- }
- function fromUnaryFilter(filter) {
- switch (filter.unaryFilter.op) {
- case 'IS_NAN':
- const nanField = fromFieldPathReference(filter.unaryFilter.field);
- return FieldFilter.create(nanField, "==" /* Operator.EQUAL */, {
- doubleValue: NaN
- });
- case 'IS_NULL':
- const nullField = fromFieldPathReference(filter.unaryFilter.field);
- return FieldFilter.create(nullField, "==" /* Operator.EQUAL */, {
- nullValue: 'NULL_VALUE'
- });
- case 'IS_NOT_NAN':
- const notNanField = fromFieldPathReference(filter.unaryFilter.field);
- return FieldFilter.create(notNanField, "!=" /* Operator.NOT_EQUAL */, {
- doubleValue: NaN
- });
- case 'IS_NOT_NULL':
- const notNullField = fromFieldPathReference(filter.unaryFilter.field);
- return FieldFilter.create(notNullField, "!=" /* Operator.NOT_EQUAL */, {
- nullValue: 'NULL_VALUE'
- });
- case 'OPERATOR_UNSPECIFIED':
- return fail();
- default:
- return fail();
- }
- }
- function fromFieldFilter(filter) {
- return FieldFilter.create(fromFieldPathReference(filter.fieldFilter.field), fromOperatorName(filter.fieldFilter.op), filter.fieldFilter.value);
- }
- function fromCompositeFilter(filter) {
- return CompositeFilter.create(filter.compositeFilter.filters.map(filter => fromFilter(filter)), fromCompositeOperatorName(filter.compositeFilter.op));
- }
- function toDocumentMask(fieldMask) {
- const canonicalFields = [];
- fieldMask.fields.forEach(field => canonicalFields.push(field.canonicalString()));
- return {
- fieldPaths: canonicalFields
- };
- }
- function fromDocumentMask(proto) {
- const paths = proto.fieldPaths || [];
- return new FieldMask(paths.map(path => FieldPath$1.fromServerFormat(path)));
- }
- function isValidResourceName(path) {
- // Resource names have at least 4 components (project ID, database ID)
- return (path.length >= 4 &&
- path.get(0) === 'projects' &&
- path.get(2) === 'databases');
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * An immutable set of metadata that the local store tracks for each target.
- */
- class TargetData {
- constructor(
- /** The target being listened to. */
- target,
- /**
- * The target ID to which the target corresponds; Assigned by the
- * LocalStore for user listens and by the SyncEngine for limbo watches.
- */
- targetId,
- /** The purpose of the target. */
- purpose,
- /**
- * The sequence number of the last transaction during which this target data
- * was modified.
- */
- sequenceNumber,
- /** The latest snapshot version seen for this target. */
- snapshotVersion = SnapshotVersion.min(),
- /**
- * The maximum snapshot version at which the associated view
- * contained no limbo documents.
- */
- lastLimboFreeSnapshotVersion = SnapshotVersion.min(),
- /**
- * An opaque, server-assigned token that allows watching a target to be
- * resumed after disconnecting without retransmitting all the data that
- * matches the target. The resume token essentially identifies a point in
- * time from which the server should resume sending results.
- */
- resumeToken = ByteString.EMPTY_BYTE_STRING,
- /**
- * The number of documents that last matched the query at the resume token or
- * read time. Documents are counted only when making a listen request with
- * resume token or read time, otherwise, keep it null.
- */
- expectedCount = null) {
- this.target = target;
- this.targetId = targetId;
- this.purpose = purpose;
- this.sequenceNumber = sequenceNumber;
- this.snapshotVersion = snapshotVersion;
- this.lastLimboFreeSnapshotVersion = lastLimboFreeSnapshotVersion;
- this.resumeToken = resumeToken;
- this.expectedCount = expectedCount;
- }
- /** Creates a new target data instance with an updated sequence number. */
- withSequenceNumber(sequenceNumber) {
- return new TargetData(this.target, this.targetId, this.purpose, sequenceNumber, this.snapshotVersion, this.lastLimboFreeSnapshotVersion, this.resumeToken, this.expectedCount);
- }
- /**
- * Creates a new target data instance with an updated resume token and
- * snapshot version.
- */
- withResumeToken(resumeToken, snapshotVersion) {
- return new TargetData(this.target, this.targetId, this.purpose, this.sequenceNumber, snapshotVersion, this.lastLimboFreeSnapshotVersion, resumeToken,
- /* expectedCount= */ null);
- }
- /**
- * Creates a new target data instance with an updated expected count.
- */
- withExpectedCount(expectedCount) {
- return new TargetData(this.target, this.targetId, this.purpose, this.sequenceNumber, this.snapshotVersion, this.lastLimboFreeSnapshotVersion, this.resumeToken, expectedCount);
- }
- /**
- * Creates a new target data instance with an updated last limbo free
- * snapshot version number.
- */
- withLastLimboFreeSnapshotVersion(lastLimboFreeSnapshotVersion) {
- return new TargetData(this.target, this.targetId, this.purpose, this.sequenceNumber, this.snapshotVersion, lastLimboFreeSnapshotVersion, this.resumeToken, this.expectedCount);
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /** Serializer for values stored in the LocalStore. */
- class LocalSerializer {
- constructor(remoteSerializer) {
- this.remoteSerializer = remoteSerializer;
- }
- }
- /** Decodes a remote document from storage locally to a Document. */
- function fromDbRemoteDocument(localSerializer, remoteDoc) {
- let doc;
- if (remoteDoc.document) {
- doc = fromDocument(localSerializer.remoteSerializer, remoteDoc.document, !!remoteDoc.hasCommittedMutations);
- }
- else if (remoteDoc.noDocument) {
- const key = DocumentKey.fromSegments(remoteDoc.noDocument.path);
- const version = fromDbTimestamp(remoteDoc.noDocument.readTime);
- doc = MutableDocument.newNoDocument(key, version);
- if (remoteDoc.hasCommittedMutations) {
- doc.setHasCommittedMutations();
- }
- }
- else if (remoteDoc.unknownDocument) {
- const key = DocumentKey.fromSegments(remoteDoc.unknownDocument.path);
- const version = fromDbTimestamp(remoteDoc.unknownDocument.version);
- doc = MutableDocument.newUnknownDocument(key, version);
- }
- else {
- return fail();
- }
- if (remoteDoc.readTime) {
- doc.setReadTime(fromDbTimestampKey(remoteDoc.readTime));
- }
- return doc;
- }
- /** Encodes a document for storage locally. */
- function toDbRemoteDocument(localSerializer, document) {
- const key = document.key;
- const remoteDoc = {
- prefixPath: key.getCollectionPath().popLast().toArray(),
- collectionGroup: key.collectionGroup,
- documentId: key.path.lastSegment(),
- readTime: toDbTimestampKey(document.readTime),
- hasCommittedMutations: document.hasCommittedMutations
- };
- if (document.isFoundDocument()) {
- remoteDoc.document = toDocument(localSerializer.remoteSerializer, document);
- }
- else if (document.isNoDocument()) {
- remoteDoc.noDocument = {
- path: key.path.toArray(),
- readTime: toDbTimestamp(document.version)
- };
- }
- else if (document.isUnknownDocument()) {
- remoteDoc.unknownDocument = {
- path: key.path.toArray(),
- version: toDbTimestamp(document.version)
- };
- }
- else {
- return fail();
- }
- return remoteDoc;
- }
- function toDbTimestampKey(snapshotVersion) {
- const timestamp = snapshotVersion.toTimestamp();
- return [timestamp.seconds, timestamp.nanoseconds];
- }
- function fromDbTimestampKey(dbTimestampKey) {
- const timestamp = new Timestamp(dbTimestampKey[0], dbTimestampKey[1]);
- return SnapshotVersion.fromTimestamp(timestamp);
- }
- function toDbTimestamp(snapshotVersion) {
- const timestamp = snapshotVersion.toTimestamp();
- return { seconds: timestamp.seconds, nanoseconds: timestamp.nanoseconds };
- }
- function fromDbTimestamp(dbTimestamp) {
- const timestamp = new Timestamp(dbTimestamp.seconds, dbTimestamp.nanoseconds);
- return SnapshotVersion.fromTimestamp(timestamp);
- }
- /** Encodes a batch of mutations into a DbMutationBatch for local storage. */
- function toDbMutationBatch(localSerializer, userId, batch) {
- const serializedBaseMutations = batch.baseMutations.map(m => toMutation(localSerializer.remoteSerializer, m));
- const serializedMutations = batch.mutations.map(m => toMutation(localSerializer.remoteSerializer, m));
- return {
- userId,
- batchId: batch.batchId,
- localWriteTimeMs: batch.localWriteTime.toMillis(),
- baseMutations: serializedBaseMutations,
- mutations: serializedMutations
- };
- }
- /** Decodes a DbMutationBatch into a MutationBatch */
- function fromDbMutationBatch(localSerializer, dbBatch) {
- const baseMutations = (dbBatch.baseMutations || []).map(m => fromMutation(localSerializer.remoteSerializer, m));
- // Squash old transform mutations into existing patch or set mutations.
- // The replacement of representing `transforms` with `update_transforms`
- // on the SDK means that old `transform` mutations stored in IndexedDB need
- // to be updated to `update_transforms`.
- // TODO(b/174608374): Remove this code once we perform a schema migration.
- for (let i = 0; i < dbBatch.mutations.length - 1; ++i) {
- const currentMutation = dbBatch.mutations[i];
- const hasTransform = i + 1 < dbBatch.mutations.length &&
- dbBatch.mutations[i + 1].transform !== undefined;
- if (hasTransform) {
- const transformMutation = dbBatch.mutations[i + 1];
- currentMutation.updateTransforms =
- transformMutation.transform.fieldTransforms;
- dbBatch.mutations.splice(i + 1, 1);
- ++i;
- }
- }
- const mutations = dbBatch.mutations.map(m => fromMutation(localSerializer.remoteSerializer, m));
- const timestamp = Timestamp.fromMillis(dbBatch.localWriteTimeMs);
- return new MutationBatch(dbBatch.batchId, timestamp, baseMutations, mutations);
- }
- /** Decodes a DbTarget into TargetData */
- function fromDbTarget(dbTarget) {
- const version = fromDbTimestamp(dbTarget.readTime);
- const lastLimboFreeSnapshotVersion = dbTarget.lastLimboFreeSnapshotVersion !== undefined
- ? fromDbTimestamp(dbTarget.lastLimboFreeSnapshotVersion)
- : SnapshotVersion.min();
- let target;
- if (isDocumentQuery(dbTarget.query)) {
- target = fromDocumentsTarget(dbTarget.query);
- }
- else {
- target = fromQueryTarget(dbTarget.query);
- }
- return new TargetData(target, dbTarget.targetId, "TargetPurposeListen" /* TargetPurpose.Listen */, dbTarget.lastListenSequenceNumber, version, lastLimboFreeSnapshotVersion, ByteString.fromBase64String(dbTarget.resumeToken));
- }
- /** Encodes TargetData into a DbTarget for storage locally. */
- function toDbTarget(localSerializer, targetData) {
- const dbTimestamp = toDbTimestamp(targetData.snapshotVersion);
- const dbLastLimboFreeTimestamp = toDbTimestamp(targetData.lastLimboFreeSnapshotVersion);
- let queryProto;
- if (targetIsDocumentTarget(targetData.target)) {
- queryProto = toDocumentsTarget(localSerializer.remoteSerializer, targetData.target);
- }
- else {
- queryProto = toQueryTarget(localSerializer.remoteSerializer, targetData.target);
- }
- // We can't store the resumeToken as a ByteString in IndexedDb, so we
- // convert it to a base64 string for storage.
- const resumeToken = targetData.resumeToken.toBase64();
- // lastListenSequenceNumber is always 0 until we do real GC.
- return {
- targetId: targetData.targetId,
- canonicalId: canonifyTarget(targetData.target),
- readTime: dbTimestamp,
- resumeToken,
- lastListenSequenceNumber: targetData.sequenceNumber,
- lastLimboFreeSnapshotVersion: dbLastLimboFreeTimestamp,
- query: queryProto
- };
- }
- /**
- * A helper function for figuring out what kind of query has been stored.
- */
- function isDocumentQuery(dbQuery) {
- return dbQuery.documents !== undefined;
- }
- /** Encodes a DbBundle to a BundleMetadata object. */
- function fromDbBundle(dbBundle) {
- return {
- id: dbBundle.bundleId,
- createTime: fromDbTimestamp(dbBundle.createTime),
- version: dbBundle.version
- };
- }
- /** Encodes a BundleMetadata to a DbBundle. */
- function toDbBundle(metadata) {
- return {
- bundleId: metadata.id,
- createTime: toDbTimestamp(fromVersion(metadata.createTime)),
- version: metadata.version
- };
- }
- /** Encodes a DbNamedQuery to a NamedQuery. */
- function fromDbNamedQuery(dbNamedQuery) {
- return {
- name: dbNamedQuery.name,
- query: fromBundledQuery(dbNamedQuery.bundledQuery),
- readTime: fromDbTimestamp(dbNamedQuery.readTime)
- };
- }
- /** Encodes a NamedQuery from a bundle proto to a DbNamedQuery. */
- function toDbNamedQuery(query) {
- return {
- name: query.name,
- readTime: toDbTimestamp(fromVersion(query.readTime)),
- bundledQuery: query.bundledQuery
- };
- }
- /**
- * Encodes a `BundledQuery` from bundle proto to a Query object.
- *
- * This reconstructs the original query used to build the bundle being loaded,
- * including features exists only in SDKs (for example: limit-to-last).
- */
- function fromBundledQuery(bundledQuery) {
- const query = convertQueryTargetToQuery({
- parent: bundledQuery.parent,
- structuredQuery: bundledQuery.structuredQuery
- });
- if (bundledQuery.limitType === 'LAST') {
- return queryWithLimit(query, query.limit, "L" /* LimitType.Last */);
- }
- return query;
- }
- /** Encodes a NamedQuery proto object to a NamedQuery model object. */
- function fromProtoNamedQuery(namedQuery) {
- return {
- name: namedQuery.name,
- query: fromBundledQuery(namedQuery.bundledQuery),
- readTime: fromVersion(namedQuery.readTime)
- };
- }
- /** Decodes a BundleMetadata proto into a BundleMetadata object. */
- function fromBundleMetadata(metadata) {
- return {
- id: metadata.id,
- version: metadata.version,
- createTime: fromVersion(metadata.createTime)
- };
- }
- /** Encodes a DbDocumentOverlay object to an Overlay model object. */
- function fromDbDocumentOverlay(localSerializer, dbDocumentOverlay) {
- return new Overlay(dbDocumentOverlay.largestBatchId, fromMutation(localSerializer.remoteSerializer, dbDocumentOverlay.overlayMutation));
- }
- /** Decodes an Overlay model object into a DbDocumentOverlay object. */
- function toDbDocumentOverlay(localSerializer, userId, overlay) {
- const [_, collectionPath, documentId] = toDbDocumentOverlayKey(userId, overlay.mutation.key);
- return {
- userId,
- collectionPath,
- documentId,
- collectionGroup: overlay.mutation.key.getCollectionGroup(),
- largestBatchId: overlay.largestBatchId,
- overlayMutation: toMutation(localSerializer.remoteSerializer, overlay.mutation)
- };
- }
- /**
- * Returns the DbDocumentOverlayKey corresponding to the given user and
- * document key.
- */
- function toDbDocumentOverlayKey(userId, docKey) {
- const docId = docKey.path.lastSegment();
- const collectionPath = encodeResourcePath(docKey.path.popLast());
- return [userId, collectionPath, docId];
- }
- function toDbIndexConfiguration(index) {
- return {
- indexId: index.indexId,
- collectionGroup: index.collectionGroup,
- fields: index.fields.map(s => [s.fieldPath.canonicalString(), s.kind])
- };
- }
- function fromDbIndexConfiguration(index, state) {
- const decodedState = state
- ? new IndexState(state.sequenceNumber, new IndexOffset(fromDbTimestamp(state.readTime), new DocumentKey(decodeResourcePath(state.documentKey)), state.largestBatchId))
- : IndexState.empty();
- const decodedSegments = index.fields.map(([fieldPath, kind]) => new IndexSegment(FieldPath$1.fromServerFormat(fieldPath), kind));
- return new FieldIndex(index.indexId, index.collectionGroup, decodedSegments, decodedState);
- }
- function toDbIndexState(indexId, user, sequenceNumber, offset) {
- return {
- indexId,
- uid: user.uid || '',
- sequenceNumber,
- readTime: toDbTimestamp(offset.readTime),
- documentKey: encodeResourcePath(offset.documentKey.path),
- largestBatchId: offset.largestBatchId
- };
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- class IndexedDbBundleCache {
- getBundleMetadata(transaction, bundleId) {
- return bundlesStore(transaction)
- .get(bundleId)
- .next(bundle => {
- if (bundle) {
- return fromDbBundle(bundle);
- }
- return undefined;
- });
- }
- saveBundleMetadata(transaction, bundleMetadata) {
- return bundlesStore(transaction).put(toDbBundle(bundleMetadata));
- }
- getNamedQuery(transaction, queryName) {
- return namedQueriesStore(transaction)
- .get(queryName)
- .next(query => {
- if (query) {
- return fromDbNamedQuery(query);
- }
- return undefined;
- });
- }
- saveNamedQuery(transaction, query) {
- return namedQueriesStore(transaction).put(toDbNamedQuery(query));
- }
- }
- /**
- * Helper to get a typed SimpleDbStore for the bundles object store.
- */
- function bundlesStore(txn) {
- return getStore(txn, DbBundleStore);
- }
- /**
- * Helper to get a typed SimpleDbStore for the namedQueries object store.
- */
- function namedQueriesStore(txn) {
- return getStore(txn, DbNamedQueryStore);
- }
- /**
- * @license
- * Copyright 2022 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Implementation of DocumentOverlayCache using IndexedDb.
- */
- class IndexedDbDocumentOverlayCache {
- /**
- * @param serializer - The document serializer.
- * @param userId - The userId for which we are accessing overlays.
- */
- constructor(serializer, userId) {
- this.serializer = serializer;
- this.userId = userId;
- }
- static forUser(serializer, user) {
- const userId = user.uid || '';
- return new IndexedDbDocumentOverlayCache(serializer, userId);
- }
- getOverlay(transaction, key) {
- return documentOverlayStore(transaction)
- .get(toDbDocumentOverlayKey(this.userId, key))
- .next(dbOverlay => {
- if (dbOverlay) {
- return fromDbDocumentOverlay(this.serializer, dbOverlay);
- }
- return null;
- });
- }
- getOverlays(transaction, keys) {
- const result = newOverlayMap();
- return PersistencePromise.forEach(keys, (key) => {
- return this.getOverlay(transaction, key).next(overlay => {
- if (overlay !== null) {
- result.set(key, overlay);
- }
- });
- }).next(() => result);
- }
- saveOverlays(transaction, largestBatchId, overlays) {
- const promises = [];
- overlays.forEach((_, mutation) => {
- const overlay = new Overlay(largestBatchId, mutation);
- promises.push(this.saveOverlay(transaction, overlay));
- });
- return PersistencePromise.waitFor(promises);
- }
- removeOverlaysForBatchId(transaction, documentKeys, batchId) {
- const collectionPaths = new Set();
- // Get the set of unique collection paths.
- documentKeys.forEach(key => collectionPaths.add(encodeResourcePath(key.getCollectionPath())));
- const promises = [];
- collectionPaths.forEach(collectionPath => {
- const range = IDBKeyRange.bound([this.userId, collectionPath, batchId], [this.userId, collectionPath, batchId + 1],
- /*lowerOpen=*/ false,
- /*upperOpen=*/ true);
- promises.push(documentOverlayStore(transaction).deleteAll(DbDocumentOverlayCollectionPathOverlayIndex, range));
- });
- return PersistencePromise.waitFor(promises);
- }
- getOverlaysForCollection(transaction, collection, sinceBatchId) {
- const result = newOverlayMap();
- const collectionPath = encodeResourcePath(collection);
- // We want batch IDs larger than `sinceBatchId`, and so the lower bound
- // is not inclusive.
- const range = IDBKeyRange.bound([this.userId, collectionPath, sinceBatchId], [this.userId, collectionPath, Number.POSITIVE_INFINITY],
- /*lowerOpen=*/ true);
- return documentOverlayStore(transaction)
- .loadAll(DbDocumentOverlayCollectionPathOverlayIndex, range)
- .next(dbOverlays => {
- for (const dbOverlay of dbOverlays) {
- const overlay = fromDbDocumentOverlay(this.serializer, dbOverlay);
- result.set(overlay.getKey(), overlay);
- }
- return result;
- });
- }
- getOverlaysForCollectionGroup(transaction, collectionGroup, sinceBatchId, count) {
- const result = newOverlayMap();
- let currentBatchId = undefined;
- // We want batch IDs larger than `sinceBatchId`, and so the lower bound
- // is not inclusive.
- const range = IDBKeyRange.bound([this.userId, collectionGroup, sinceBatchId], [this.userId, collectionGroup, Number.POSITIVE_INFINITY],
- /*lowerOpen=*/ true);
- return documentOverlayStore(transaction)
- .iterate({
- index: DbDocumentOverlayCollectionGroupOverlayIndex,
- range
- }, (_, dbOverlay, control) => {
- // We do not want to return partial batch overlays, even if the size
- // of the result set exceeds the given `count` argument. Therefore, we
- // continue to aggregate results even after the result size exceeds
- // `count` if there are more overlays from the `currentBatchId`.
- const overlay = fromDbDocumentOverlay(this.serializer, dbOverlay);
- if (result.size() < count ||
- overlay.largestBatchId === currentBatchId) {
- result.set(overlay.getKey(), overlay);
- currentBatchId = overlay.largestBatchId;
- }
- else {
- control.done();
- }
- })
- .next(() => result);
- }
- saveOverlay(transaction, overlay) {
- return documentOverlayStore(transaction).put(toDbDocumentOverlay(this.serializer, this.userId, overlay));
- }
- }
- /**
- * Helper to get a typed SimpleDbStore for the document overlay object store.
- */
- function documentOverlayStore(txn) {
- return getStore(txn, DbDocumentOverlayStore);
- }
- /**
- * @license
- * Copyright 2021 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- // Note: This code is copied from the backend. Code that is not used by
- // Firestore was removed.
- const INDEX_TYPE_NULL = 5;
- const INDEX_TYPE_BOOLEAN = 10;
- const INDEX_TYPE_NAN = 13;
- const INDEX_TYPE_NUMBER = 15;
- const INDEX_TYPE_TIMESTAMP = 20;
- const INDEX_TYPE_STRING = 25;
- const INDEX_TYPE_BLOB = 30;
- const INDEX_TYPE_REFERENCE = 37;
- const INDEX_TYPE_GEOPOINT = 45;
- const INDEX_TYPE_ARRAY = 50;
- const INDEX_TYPE_MAP = 55;
- const INDEX_TYPE_REFERENCE_SEGMENT = 60;
- // A terminator that indicates that a truncatable value was not truncated.
- // This must be smaller than all other type labels.
- const NOT_TRUNCATED = 2;
- /** Firestore index value writer. */
- class FirestoreIndexValueWriter {
- constructor() { }
- // The write methods below short-circuit writing terminators for values
- // containing a (terminating) truncated value.
- //
- // As an example, consider the resulting encoding for:
- //
- // ["bar", [2, "foo"]] -> (STRING, "bar", TERM, ARRAY, NUMBER, 2, STRING, "foo", TERM, TERM, TERM)
- // ["bar", [2, truncated("foo")]] -> (STRING, "bar", TERM, ARRAY, NUMBER, 2, STRING, "foo", TRUNC)
- // ["bar", truncated(["foo"])] -> (STRING, "bar", TERM, ARRAY. STRING, "foo", TERM, TRUNC)
- /** Writes an index value. */
- writeIndexValue(value, encoder) {
- this.writeIndexValueAux(value, encoder);
- // Write separator to split index values
- // (see go/firestore-storage-format#encodings).
- encoder.writeInfinity();
- }
- writeIndexValueAux(indexValue, encoder) {
- if ('nullValue' in indexValue) {
- this.writeValueTypeLabel(encoder, INDEX_TYPE_NULL);
- }
- else if ('booleanValue' in indexValue) {
- this.writeValueTypeLabel(encoder, INDEX_TYPE_BOOLEAN);
- encoder.writeNumber(indexValue.booleanValue ? 1 : 0);
- }
- else if ('integerValue' in indexValue) {
- this.writeValueTypeLabel(encoder, INDEX_TYPE_NUMBER);
- encoder.writeNumber(normalizeNumber(indexValue.integerValue));
- }
- else if ('doubleValue' in indexValue) {
- const n = normalizeNumber(indexValue.doubleValue);
- if (isNaN(n)) {
- this.writeValueTypeLabel(encoder, INDEX_TYPE_NAN);
- }
- else {
- this.writeValueTypeLabel(encoder, INDEX_TYPE_NUMBER);
- if (isNegativeZero(n)) {
- // -0.0, 0 and 0.0 are all considered the same
- encoder.writeNumber(0.0);
- }
- else {
- encoder.writeNumber(n);
- }
- }
- }
- else if ('timestampValue' in indexValue) {
- const timestamp = indexValue.timestampValue;
- this.writeValueTypeLabel(encoder, INDEX_TYPE_TIMESTAMP);
- if (typeof timestamp === 'string') {
- encoder.writeString(timestamp);
- }
- else {
- encoder.writeString(`${timestamp.seconds || ''}`);
- encoder.writeNumber(timestamp.nanos || 0);
- }
- }
- else if ('stringValue' in indexValue) {
- this.writeIndexString(indexValue.stringValue, encoder);
- this.writeTruncationMarker(encoder);
- }
- else if ('bytesValue' in indexValue) {
- this.writeValueTypeLabel(encoder, INDEX_TYPE_BLOB);
- encoder.writeBytes(normalizeByteString(indexValue.bytesValue));
- this.writeTruncationMarker(encoder);
- }
- else if ('referenceValue' in indexValue) {
- this.writeIndexEntityRef(indexValue.referenceValue, encoder);
- }
- else if ('geoPointValue' in indexValue) {
- const geoPoint = indexValue.geoPointValue;
- this.writeValueTypeLabel(encoder, INDEX_TYPE_GEOPOINT);
- encoder.writeNumber(geoPoint.latitude || 0);
- encoder.writeNumber(geoPoint.longitude || 0);
- }
- else if ('mapValue' in indexValue) {
- if (isMaxValue(indexValue)) {
- this.writeValueTypeLabel(encoder, Number.MAX_SAFE_INTEGER);
- }
- else {
- this.writeIndexMap(indexValue.mapValue, encoder);
- this.writeTruncationMarker(encoder);
- }
- }
- else if ('arrayValue' in indexValue) {
- this.writeIndexArray(indexValue.arrayValue, encoder);
- this.writeTruncationMarker(encoder);
- }
- else {
- fail();
- }
- }
- writeIndexString(stringIndexValue, encoder) {
- this.writeValueTypeLabel(encoder, INDEX_TYPE_STRING);
- this.writeUnlabeledIndexString(stringIndexValue, encoder);
- }
- writeUnlabeledIndexString(stringIndexValue, encoder) {
- encoder.writeString(stringIndexValue);
- }
- writeIndexMap(mapIndexValue, encoder) {
- const map = mapIndexValue.fields || {};
- this.writeValueTypeLabel(encoder, INDEX_TYPE_MAP);
- for (const key of Object.keys(map)) {
- this.writeIndexString(key, encoder);
- this.writeIndexValueAux(map[key], encoder);
- }
- }
- writeIndexArray(arrayIndexValue, encoder) {
- const values = arrayIndexValue.values || [];
- this.writeValueTypeLabel(encoder, INDEX_TYPE_ARRAY);
- for (const element of values) {
- this.writeIndexValueAux(element, encoder);
- }
- }
- writeIndexEntityRef(referenceValue, encoder) {
- this.writeValueTypeLabel(encoder, INDEX_TYPE_REFERENCE);
- const path = DocumentKey.fromName(referenceValue).path;
- path.forEach(segment => {
- this.writeValueTypeLabel(encoder, INDEX_TYPE_REFERENCE_SEGMENT);
- this.writeUnlabeledIndexString(segment, encoder);
- });
- }
- writeValueTypeLabel(encoder, typeOrder) {
- encoder.writeNumber(typeOrder);
- }
- writeTruncationMarker(encoder) {
- // While the SDK does not implement truncation, the truncation marker is
- // used to terminate all variable length values (which are strings, bytes,
- // references, arrays and maps).
- encoder.writeNumber(NOT_TRUNCATED);
- }
- }
- FirestoreIndexValueWriter.INSTANCE = new FirestoreIndexValueWriter();
- /**
- * @license
- * Copyright 2021 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law | agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES | CONDITIONS OF ANY KIND, either express | implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /** These constants are taken from the backend. */
- const MIN_SURROGATE = '\uD800';
- const MAX_SURROGATE = '\uDBFF';
- const ESCAPE1 = 0x00;
- const NULL_BYTE = 0xff; // Combined with ESCAPE1
- const SEPARATOR = 0x01; // Combined with ESCAPE1
- const ESCAPE2 = 0xff;
- const INFINITY = 0xff; // Combined with ESCAPE2
- const FF_BYTE = 0x00; // Combined with ESCAPE2
- const LONG_SIZE = 64;
- const BYTE_SIZE = 8;
- /**
- * The default size of the buffer. This is arbitrary, but likely larger than
- * most index values so that less copies of the underlying buffer will be made.
- * For large values, a single copy will made to double the buffer length.
- */
- const DEFAULT_BUFFER_SIZE = 1024;
- /** Converts a JavaScript number to a byte array (using big endian encoding). */
- function doubleToLongBits(value) {
- const dv = new DataView(new ArrayBuffer(8));
- dv.setFloat64(0, value, /* littleEndian= */ false);
- return new Uint8Array(dv.buffer);
- }
- /**
- * Counts the number of zeros in a byte.
- *
- * Visible for testing.
- */
- function numberOfLeadingZerosInByte(x) {
- if (x === 0) {
- return 8;
- }
- let zeros = 0;
- if (x >> 4 === 0) {
- // Test if the first four bits are zero.
- zeros += 4;
- x = x << 4;
- }
- if (x >> 6 === 0) {
- // Test if the first two (or next two) bits are zero.
- zeros += 2;
- x = x << 2;
- }
- if (x >> 7 === 0) {
- // Test if the remaining bit is zero.
- zeros += 1;
- }
- return zeros;
- }
- /** Counts the number of leading zeros in the given byte array. */
- function numberOfLeadingZeros(bytes) {
- let leadingZeros = 0;
- for (let i = 0; i < 8; ++i) {
- const zeros = numberOfLeadingZerosInByte(bytes[i] & 0xff);
- leadingZeros += zeros;
- if (zeros !== 8) {
- break;
- }
- }
- return leadingZeros;
- }
- /**
- * Returns the number of bytes required to store "value". Leading zero bytes
- * are skipped.
- */
- function unsignedNumLength(value) {
- // This is just the number of bytes for the unsigned representation of the number.
- const numBits = LONG_SIZE - numberOfLeadingZeros(value);
- return Math.ceil(numBits / BYTE_SIZE);
- }
- /**
- * OrderedCodeWriter is a minimal-allocation implementation of the writing
- * behavior defined by the backend.
- *
- * The code is ported from its Java counterpart.
- */
- class OrderedCodeWriter {
- constructor() {
- this.buffer = new Uint8Array(DEFAULT_BUFFER_SIZE);
- this.position = 0;
- }
- writeBytesAscending(value) {
- const it = value[Symbol.iterator]();
- let byte = it.next();
- while (!byte.done) {
- this.writeByteAscending(byte.value);
- byte = it.next();
- }
- this.writeSeparatorAscending();
- }
- writeBytesDescending(value) {
- const it = value[Symbol.iterator]();
- let byte = it.next();
- while (!byte.done) {
- this.writeByteDescending(byte.value);
- byte = it.next();
- }
- this.writeSeparatorDescending();
- }
- /** Writes utf8 bytes into this byte sequence, ascending. */
- writeUtf8Ascending(sequence) {
- for (const c of sequence) {
- const charCode = c.charCodeAt(0);
- if (charCode < 0x80) {
- this.writeByteAscending(charCode);
- }
- else if (charCode < 0x800) {
- this.writeByteAscending((0x0f << 6) | (charCode >>> 6));
- this.writeByteAscending(0x80 | (0x3f & charCode));
- }
- else if (c < MIN_SURROGATE || MAX_SURROGATE < c) {
- this.writeByteAscending((0x0f << 5) | (charCode >>> 12));
- this.writeByteAscending(0x80 | (0x3f & (charCode >>> 6)));
- this.writeByteAscending(0x80 | (0x3f & charCode));
- }
- else {
- const codePoint = c.codePointAt(0);
- this.writeByteAscending((0x0f << 4) | (codePoint >>> 18));
- this.writeByteAscending(0x80 | (0x3f & (codePoint >>> 12)));
- this.writeByteAscending(0x80 | (0x3f & (codePoint >>> 6)));
- this.writeByteAscending(0x80 | (0x3f & codePoint));
- }
- }
- this.writeSeparatorAscending();
- }
- /** Writes utf8 bytes into this byte sequence, descending */
- writeUtf8Descending(sequence) {
- for (const c of sequence) {
- const charCode = c.charCodeAt(0);
- if (charCode < 0x80) {
- this.writeByteDescending(charCode);
- }
- else if (charCode < 0x800) {
- this.writeByteDescending((0x0f << 6) | (charCode >>> 6));
- this.writeByteDescending(0x80 | (0x3f & charCode));
- }
- else if (c < MIN_SURROGATE || MAX_SURROGATE < c) {
- this.writeByteDescending((0x0f << 5) | (charCode >>> 12));
- this.writeByteDescending(0x80 | (0x3f & (charCode >>> 6)));
- this.writeByteDescending(0x80 | (0x3f & charCode));
- }
- else {
- const codePoint = c.codePointAt(0);
- this.writeByteDescending((0x0f << 4) | (codePoint >>> 18));
- this.writeByteDescending(0x80 | (0x3f & (codePoint >>> 12)));
- this.writeByteDescending(0x80 | (0x3f & (codePoint >>> 6)));
- this.writeByteDescending(0x80 | (0x3f & codePoint));
- }
- }
- this.writeSeparatorDescending();
- }
- writeNumberAscending(val) {
- // Values are encoded with a single byte length prefix, followed by the
- // actual value in big-endian format with leading 0 bytes dropped.
- const value = this.toOrderedBits(val);
- const len = unsignedNumLength(value);
- this.ensureAvailable(1 + len);
- this.buffer[this.position++] = len & 0xff; // Write the length
- for (let i = value.length - len; i < value.length; ++i) {
- this.buffer[this.position++] = value[i] & 0xff;
- }
- }
- writeNumberDescending(val) {
- // Values are encoded with a single byte length prefix, followed by the
- // inverted value in big-endian format with leading 0 bytes dropped.
- const value = this.toOrderedBits(val);
- const len = unsignedNumLength(value);
- this.ensureAvailable(1 + len);
- this.buffer[this.position++] = ~(len & 0xff); // Write the length
- for (let i = value.length - len; i < value.length; ++i) {
- this.buffer[this.position++] = ~(value[i] & 0xff);
- }
- }
- /**
- * Writes the "infinity" byte sequence that sorts after all other byte
- * sequences written in ascending order.
- */
- writeInfinityAscending() {
- this.writeEscapedByteAscending(ESCAPE2);
- this.writeEscapedByteAscending(INFINITY);
- }
- /**
- * Writes the "infinity" byte sequence that sorts before all other byte
- * sequences written in descending order.
- */
- writeInfinityDescending() {
- this.writeEscapedByteDescending(ESCAPE2);
- this.writeEscapedByteDescending(INFINITY);
- }
- /**
- * Resets the buffer such that it is the same as when it was newly
- * constructed.
- */
- reset() {
- this.position = 0;
- }
- seed(encodedBytes) {
- this.ensureAvailable(encodedBytes.length);
- this.buffer.set(encodedBytes, this.position);
- this.position += encodedBytes.length;
- }
- /** Makes a copy of the encoded bytes in this buffer. */
- encodedBytes() {
- return this.buffer.slice(0, this.position);
- }
- /**
- * Encodes `val` into an encoding so that the order matches the IEEE 754
- * floating-point comparison results with the following exceptions:
- * -0.0 < 0.0
- * all non-NaN < NaN
- * NaN = NaN
- */
- toOrderedBits(val) {
- const value = doubleToLongBits(val);
- // Check if the first bit is set. We use a bit mask since value[0] is
- // encoded as a number from 0 to 255.
- const isNegative = (value[0] & 0x80) !== 0;
- // Revert the two complement to get natural ordering
- value[0] ^= isNegative ? 0xff : 0x80;
- for (let i = 1; i < value.length; ++i) {
- value[i] ^= isNegative ? 0xff : 0x00;
- }
- return value;
- }
- /** Writes a single byte ascending to the buffer. */
- writeByteAscending(b) {
- const masked = b & 0xff;
- if (masked === ESCAPE1) {
- this.writeEscapedByteAscending(ESCAPE1);
- this.writeEscapedByteAscending(NULL_BYTE);
- }
- else if (masked === ESCAPE2) {
- this.writeEscapedByteAscending(ESCAPE2);
- this.writeEscapedByteAscending(FF_BYTE);
- }
- else {
- this.writeEscapedByteAscending(masked);
- }
- }
- /** Writes a single byte descending to the buffer. */
- writeByteDescending(b) {
- const masked = b & 0xff;
- if (masked === ESCAPE1) {
- this.writeEscapedByteDescending(ESCAPE1);
- this.writeEscapedByteDescending(NULL_BYTE);
- }
- else if (masked === ESCAPE2) {
- this.writeEscapedByteDescending(ESCAPE2);
- this.writeEscapedByteDescending(FF_BYTE);
- }
- else {
- this.writeEscapedByteDescending(b);
- }
- }
- writeSeparatorAscending() {
- this.writeEscapedByteAscending(ESCAPE1);
- this.writeEscapedByteAscending(SEPARATOR);
- }
- writeSeparatorDescending() {
- this.writeEscapedByteDescending(ESCAPE1);
- this.writeEscapedByteDescending(SEPARATOR);
- }
- writeEscapedByteAscending(b) {
- this.ensureAvailable(1);
- this.buffer[this.position++] = b;
- }
- writeEscapedByteDescending(b) {
- this.ensureAvailable(1);
- this.buffer[this.position++] = ~b;
- }
- ensureAvailable(bytes) {
- const minCapacity = bytes + this.position;
- if (minCapacity <= this.buffer.length) {
- return;
- }
- // Try doubling.
- let newLength = this.buffer.length * 2;
- // Still not big enough? Just allocate the right size.
- if (newLength < minCapacity) {
- newLength = minCapacity;
- }
- // Create the new buffer.
- const newBuffer = new Uint8Array(newLength);
- newBuffer.set(this.buffer); // copy old data
- this.buffer = newBuffer;
- }
- }
- class AscendingIndexByteEncoder {
- constructor(orderedCode) {
- this.orderedCode = orderedCode;
- }
- writeBytes(value) {
- this.orderedCode.writeBytesAscending(value);
- }
- writeString(value) {
- this.orderedCode.writeUtf8Ascending(value);
- }
- writeNumber(value) {
- this.orderedCode.writeNumberAscending(value);
- }
- writeInfinity() {
- this.orderedCode.writeInfinityAscending();
- }
- }
- class DescendingIndexByteEncoder {
- constructor(orderedCode) {
- this.orderedCode = orderedCode;
- }
- writeBytes(value) {
- this.orderedCode.writeBytesDescending(value);
- }
- writeString(value) {
- this.orderedCode.writeUtf8Descending(value);
- }
- writeNumber(value) {
- this.orderedCode.writeNumberDescending(value);
- }
- writeInfinity() {
- this.orderedCode.writeInfinityDescending();
- }
- }
- /**
- * Implements `DirectionalIndexByteEncoder` using `OrderedCodeWriter` for the
- * actual encoding.
- */
- class IndexByteEncoder {
- constructor() {
- this.orderedCode = new OrderedCodeWriter();
- this.ascending = new AscendingIndexByteEncoder(this.orderedCode);
- this.descending = new DescendingIndexByteEncoder(this.orderedCode);
- }
- seed(encodedBytes) {
- this.orderedCode.seed(encodedBytes);
- }
- forKind(kind) {
- return kind === 0 /* IndexKind.ASCENDING */ ? this.ascending : this.descending;
- }
- encodedBytes() {
- return this.orderedCode.encodedBytes();
- }
- reset() {
- this.orderedCode.reset();
- }
- }
- /**
- * @license
- * Copyright 2022 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /** Represents an index entry saved by the SDK in persisted storage. */
- class IndexEntry {
- constructor(indexId, documentKey, arrayValue, directionalValue) {
- this.indexId = indexId;
- this.documentKey = documentKey;
- this.arrayValue = arrayValue;
- this.directionalValue = directionalValue;
- }
- /**
- * Returns an IndexEntry entry that sorts immediately after the current
- * directional value.
- */
- successor() {
- const currentLength = this.directionalValue.length;
- const newLength = currentLength === 0 || this.directionalValue[currentLength - 1] === 255
- ? currentLength + 1
- : currentLength;
- const successor = new Uint8Array(newLength);
- successor.set(this.directionalValue, 0);
- if (newLength !== currentLength) {
- successor.set([0], this.directionalValue.length);
- }
- else {
- ++successor[successor.length - 1];
- }
- return new IndexEntry(this.indexId, this.documentKey, this.arrayValue, successor);
- }
- }
- function indexEntryComparator(left, right) {
- let cmp = left.indexId - right.indexId;
- if (cmp !== 0) {
- return cmp;
- }
- cmp = compareByteArrays(left.arrayValue, right.arrayValue);
- if (cmp !== 0) {
- return cmp;
- }
- cmp = compareByteArrays(left.directionalValue, right.directionalValue);
- if (cmp !== 0) {
- return cmp;
- }
- return DocumentKey.comparator(left.documentKey, right.documentKey);
- }
- function compareByteArrays(left, right) {
- for (let i = 0; i < left.length && i < right.length; ++i) {
- const compare = left[i] - right[i];
- if (compare !== 0) {
- return compare;
- }
- }
- return left.length - right.length;
- }
- /**
- * @license
- * Copyright 2022 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * A light query planner for Firestore.
- *
- * This class matches a `FieldIndex` against a Firestore Query `Target`. It
- * determines whether a given index can be used to serve the specified target.
- *
- * The following table showcases some possible index configurations:
- *
- * Query | Index
- * -----------------------------------------------------------------------------
- * where('a', '==', 'a').where('b', '==', 'b') | a ASC, b DESC
- * where('a', '==', 'a').where('b', '==', 'b') | a ASC
- * where('a', '==', 'a').where('b', '==', 'b') | b DESC
- * where('a', '>=', 'a').orderBy('a') | a ASC
- * where('a', '>=', 'a').orderBy('a', 'desc') | a DESC
- * where('a', '>=', 'a').orderBy('a').orderBy('b') | a ASC, b ASC
- * where('a', '>=', 'a').orderBy('a').orderBy('b') | a ASC
- * where('a', 'array-contains', 'a').orderBy('b') | a CONTAINS, b ASCENDING
- * where('a', 'array-contains', 'a').orderBy('b') | a CONTAINS
- */
- class TargetIndexMatcher {
- constructor(target) {
- this.collectionId =
- target.collectionGroup != null
- ? target.collectionGroup
- : target.path.lastSegment();
- this.orderBys = target.orderBy;
- this.equalityFilters = [];
- for (const filter of target.filters) {
- const fieldFilter = filter;
- if (fieldFilter.isInequality()) {
- this.inequalityFilter = fieldFilter;
- }
- else {
- this.equalityFilters.push(fieldFilter);
- }
- }
- }
- /**
- * Returns whether the index can be used to serve the TargetIndexMatcher's
- * target.
- *
- * An index is considered capable of serving the target when:
- * - The target uses all index segments for its filters and orderBy clauses.
- * The target can have additional filter and orderBy clauses, but not
- * fewer.
- * - If an ArrayContains/ArrayContainsAnyfilter is used, the index must also
- * have a corresponding `CONTAINS` segment.
- * - All directional index segments can be mapped to the target as a series of
- * equality filters, a single inequality filter and a series of orderBy
- * clauses.
- * - The segments that represent the equality filters may appear out of order.
- * - The optional segment for the inequality filter must appear after all
- * equality segments.
- * - The segments that represent that orderBy clause of the target must appear
- * in order after all equality and inequality segments. Single orderBy
- * clauses cannot be skipped, but a continuous orderBy suffix may be
- * omitted.
- */
- servedByIndex(index) {
- hardAssert(index.collectionGroup === this.collectionId);
- // If there is an array element, find a matching filter.
- const arraySegment = fieldIndexGetArraySegment(index);
- if (arraySegment !== undefined &&
- !this.hasMatchingEqualityFilter(arraySegment)) {
- return false;
- }
- const segments = fieldIndexGetDirectionalSegments(index);
- let equalitySegments = new Set();
- let segmentIndex = 0;
- let orderBysIndex = 0;
- // Process all equalities first. Equalities can appear out of order.
- for (; segmentIndex < segments.length; ++segmentIndex) {
- // We attempt to greedily match all segments to equality filters. If a
- // filter matches an index segment, we can mark the segment as used.
- if (this.hasMatchingEqualityFilter(segments[segmentIndex])) {
- equalitySegments = equalitySegments.add(segments[segmentIndex].fieldPath.canonicalString());
- }
- else {
- // If we cannot find a matching filter, we need to verify whether the
- // remaining segments map to the target's inequality and its orderBy
- // clauses.
- break;
- }
- }
- // If we already have processed all segments, all segments are used to serve
- // the equality filters and we do not need to map any segments to the
- // target's inequality and orderBy clauses.
- if (segmentIndex === segments.length) {
- return true;
- }
- if (this.inequalityFilter !== undefined) {
- // If there is an inequality filter and the field was not in one of the
- // equality filters above, the next segment must match both the filter
- // and the first orderBy clause.
- if (!equalitySegments.has(this.inequalityFilter.field.canonicalString())) {
- const segment = segments[segmentIndex];
- if (!this.matchesFilter(this.inequalityFilter, segment) ||
- !this.matchesOrderBy(this.orderBys[orderBysIndex++], segment)) {
- return false;
- }
- }
- ++segmentIndex;
- }
- // All remaining segments need to represent the prefix of the target's
- // orderBy.
- for (; segmentIndex < segments.length; ++segmentIndex) {
- const segment = segments[segmentIndex];
- if (orderBysIndex >= this.orderBys.length ||
- !this.matchesOrderBy(this.orderBys[orderBysIndex++], segment)) {
- return false;
- }
- }
- return true;
- }
- hasMatchingEqualityFilter(segment) {
- for (const filter of this.equalityFilters) {
- if (this.matchesFilter(filter, segment)) {
- return true;
- }
- }
- return false;
- }
- matchesFilter(filter, segment) {
- if (filter === undefined || !filter.field.isEqual(segment.fieldPath)) {
- return false;
- }
- const isArrayOperator = filter.op === "array-contains" /* Operator.ARRAY_CONTAINS */ ||
- filter.op === "array-contains-any" /* Operator.ARRAY_CONTAINS_ANY */;
- return (segment.kind === 2 /* IndexKind.CONTAINS */) === isArrayOperator;
- }
- matchesOrderBy(orderBy, segment) {
- if (!orderBy.field.isEqual(segment.fieldPath)) {
- return false;
- }
- return ((segment.kind === 0 /* IndexKind.ASCENDING */ &&
- orderBy.dir === "asc" /* Direction.ASCENDING */) ||
- (segment.kind === 1 /* IndexKind.DESCENDING */ &&
- orderBy.dir === "desc" /* Direction.DESCENDING */));
- }
- }
- /**
- * @license
- * Copyright 2022 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Provides utility functions that help with boolean logic transformations needed for handling
- * complex filters used in queries.
- */
- /**
- * The `in` filter is only a syntactic sugar over a disjunction of equalities. For instance: `a in
- * [1,2,3]` is in fact `a==1 || a==2 || a==3`. This method expands any `in` filter in the given
- * input into a disjunction of equality filters and returns the expanded filter.
- */
- function computeInExpansion(filter) {
- var _a, _b;
- hardAssert(filter instanceof FieldFilter || filter instanceof CompositeFilter);
- if (filter instanceof FieldFilter) {
- if (filter instanceof InFilter) {
- const expandedFilters = ((_b = (_a = filter.value.arrayValue) === null || _a === void 0 ? void 0 : _a.values) === null || _b === void 0 ? void 0 : _b.map(value => FieldFilter.create(filter.field, "==" /* Operator.EQUAL */, value))) || [];
- return CompositeFilter.create(expandedFilters, "or" /* CompositeOperator.OR */);
- }
- else {
- // We have reached other kinds of field filters.
- return filter;
- }
- }
- // We have a composite filter.
- const expandedFilters = filter.filters.map(subfilter => computeInExpansion(subfilter));
- return CompositeFilter.create(expandedFilters, filter.op);
- }
- /**
- * Given a composite filter, returns the list of terms in its disjunctive normal form.
- *
- * <p>Each element in the return value is one term of the resulting DNF. For instance: For the
- * input: (A || B) && C, the DNF form is: (A && C) || (B && C), and the return value is a list
- * with two elements: a composite filter that performs (A && C), and a composite filter that
- * performs (B && C).
- *
- * @param filter the composite filter to calculate DNF transform for.
- * @return the terms in the DNF transform.
- */
- function getDnfTerms(filter) {
- if (filter.getFilters().length === 0) {
- return [];
- }
- const result = computeDistributedNormalForm(computeInExpansion(filter));
- hardAssert(isDisjunctiveNormalForm(result));
- if (isSingleFieldFilter(result) || isFlatConjunction(result)) {
- return [result];
- }
- return result.getFilters();
- }
- /** Returns true if the given filter is a single field filter. e.g. (a == 10). */
- function isSingleFieldFilter(filter) {
- return filter instanceof FieldFilter;
- }
- /**
- * Returns true if the given filter is the conjunction of one or more field filters. e.g. (a == 10
- * && b == 20)
- */
- function isFlatConjunction(filter) {
- return (filter instanceof CompositeFilter &&
- compositeFilterIsFlatConjunction(filter));
- }
- /**
- * Returns whether or not the given filter is in disjunctive normal form (DNF).
- *
- * <p>In boolean logic, a disjunctive normal form (DNF) is a canonical normal form of a logical
- * formula consisting of a disjunction of conjunctions; it can also be described as an OR of ANDs.
- *
- * <p>For more info, visit: https://en.wikipedia.org/wiki/Disjunctive_normal_form
- */
- function isDisjunctiveNormalForm(filter) {
- return (isSingleFieldFilter(filter) ||
- isFlatConjunction(filter) ||
- isDisjunctionOfFieldFiltersAndFlatConjunctions(filter));
- }
- /**
- * Returns true if the given filter is the disjunction of one or more "flat conjunctions" and
- * field filters. e.g. (a == 10) || (b==20 && c==30)
- */
- function isDisjunctionOfFieldFiltersAndFlatConjunctions(filter) {
- if (filter instanceof CompositeFilter) {
- if (compositeFilterIsDisjunction(filter)) {
- for (const subFilter of filter.getFilters()) {
- if (!isSingleFieldFilter(subFilter) && !isFlatConjunction(subFilter)) {
- return false;
- }
- }
- return true;
- }
- }
- return false;
- }
- function computeDistributedNormalForm(filter) {
- hardAssert(filter instanceof FieldFilter || filter instanceof CompositeFilter);
- if (filter instanceof FieldFilter) {
- return filter;
- }
- if (filter.filters.length === 1) {
- return computeDistributedNormalForm(filter.filters[0]);
- }
- // Compute DNF for each of the subfilters first
- const result = filter.filters.map(subfilter => computeDistributedNormalForm(subfilter));
- let newFilter = CompositeFilter.create(result, filter.op);
- newFilter = applyAssociation(newFilter);
- if (isDisjunctiveNormalForm(newFilter)) {
- return newFilter;
- }
- hardAssert(newFilter instanceof CompositeFilter);
- hardAssert(compositeFilterIsConjunction(newFilter));
- hardAssert(newFilter.filters.length > 1);
- return newFilter.filters.reduce((runningResult, filter) => applyDistribution(runningResult, filter));
- }
- function applyDistribution(lhs, rhs) {
- hardAssert(lhs instanceof FieldFilter || lhs instanceof CompositeFilter);
- hardAssert(rhs instanceof FieldFilter || rhs instanceof CompositeFilter);
- let result;
- if (lhs instanceof FieldFilter) {
- if (rhs instanceof FieldFilter) {
- // FieldFilter FieldFilter
- result = applyDistributionFieldFilters(lhs, rhs);
- }
- else {
- // FieldFilter CompositeFilter
- result = applyDistributionFieldAndCompositeFilters(lhs, rhs);
- }
- }
- else {
- if (rhs instanceof FieldFilter) {
- // CompositeFilter FieldFilter
- result = applyDistributionFieldAndCompositeFilters(rhs, lhs);
- }
- else {
- // CompositeFilter CompositeFilter
- result = applyDistributionCompositeFilters(lhs, rhs);
- }
- }
- return applyAssociation(result);
- }
- function applyDistributionFieldFilters(lhs, rhs) {
- // Conjunction distribution for two field filters is the conjunction of them.
- return CompositeFilter.create([lhs, rhs], "and" /* CompositeOperator.AND */);
- }
- function applyDistributionCompositeFilters(lhs, rhs) {
- hardAssert(lhs.filters.length > 0 && rhs.filters.length > 0);
- // There are four cases:
- // (A & B) & (C & D) --> (A & B & C & D)
- // (A & B) & (C | D) --> (A & B & C) | (A & B & D)
- // (A | B) & (C & D) --> (C & D & A) | (C & D & B)
- // (A | B) & (C | D) --> (A & C) | (A & D) | (B & C) | (B & D)
- // Case 1 is a merge.
- if (compositeFilterIsConjunction(lhs) && compositeFilterIsConjunction(rhs)) {
- return compositeFilterWithAddedFilters(lhs, rhs.getFilters());
- }
- // Case 2,3,4 all have at least one side (lhs or rhs) that is a disjunction. In all three cases
- // we should take each element of the disjunction and distribute it over the other side, and
- // return the disjunction of the distribution results.
- const disjunctionSide = compositeFilterIsDisjunction(lhs) ? lhs : rhs;
- const otherSide = compositeFilterIsDisjunction(lhs) ? rhs : lhs;
- const results = disjunctionSide.filters.map(subfilter => applyDistribution(subfilter, otherSide));
- return CompositeFilter.create(results, "or" /* CompositeOperator.OR */);
- }
- function applyDistributionFieldAndCompositeFilters(fieldFilter, compositeFilter) {
- // There are two cases:
- // A & (B & C) --> (A & B & C)
- // A & (B | C) --> (A & B) | (A & C)
- if (compositeFilterIsConjunction(compositeFilter)) {
- // Case 1
- return compositeFilterWithAddedFilters(compositeFilter, fieldFilter.getFilters());
- }
- else {
- // Case 2
- const newFilters = compositeFilter.filters.map(subfilter => applyDistribution(fieldFilter, subfilter));
- return CompositeFilter.create(newFilters, "or" /* CompositeOperator.OR */);
- }
- }
- /**
- * Applies the associativity property to the given filter and returns the resulting filter.
- *
- * <ul>
- * <li>A | (B | C) == (A | B) | C == (A | B | C)
- * <li>A & (B & C) == (A & B) & C == (A & B & C)
- * </ul>
- *
- * <p>For more info, visit: https://en.wikipedia.org/wiki/Associative_property#Propositional_logic
- */
- function applyAssociation(filter) {
- hardAssert(filter instanceof FieldFilter || filter instanceof CompositeFilter);
- if (filter instanceof FieldFilter) {
- return filter;
- }
- const filters = filter.getFilters();
- // If the composite filter only contains 1 filter, apply associativity to it.
- if (filters.length === 1) {
- return applyAssociation(filters[0]);
- }
- // Associativity applied to a flat composite filter results is itself.
- if (compositeFilterIsFlat(filter)) {
- return filter;
- }
- // First apply associativity to all subfilters. This will in turn recursively apply
- // associativity to all nested composite filters and field filters.
- const updatedFilters = filters.map(subfilter => applyAssociation(subfilter));
- // For composite subfilters that perform the same kind of logical operation as `compositeFilter`
- // take out their filters and add them to `compositeFilter`. For example:
- // compositeFilter = (A | (B | C | D))
- // compositeSubfilter = (B | C | D)
- // Result: (A | B | C | D)
- // Note that the `compositeSubfilter` has been eliminated, and its filters (B, C, D) have been
- // added to the top-level "compositeFilter".
- const newSubfilters = [];
- updatedFilters.forEach(subfilter => {
- if (subfilter instanceof FieldFilter) {
- newSubfilters.push(subfilter);
- }
- else if (subfilter instanceof CompositeFilter) {
- if (subfilter.op === filter.op) {
- // compositeFilter: (A | (B | C))
- // compositeSubfilter: (B | C)
- // Result: (A | B | C)
- newSubfilters.push(...subfilter.filters);
- }
- else {
- // compositeFilter: (A | (B & C))
- // compositeSubfilter: (B & C)
- // Result: (A | (B & C))
- newSubfilters.push(subfilter);
- }
- }
- });
- if (newSubfilters.length === 1) {
- return newSubfilters[0];
- }
- return CompositeFilter.create(newSubfilters, filter.op);
- }
- /**
- * @license
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * An in-memory implementation of IndexManager.
- */
- class MemoryIndexManager {
- constructor() {
- this.collectionParentIndex = new MemoryCollectionParentIndex();
- }
- addToCollectionParentIndex(transaction, collectionPath) {
- this.collectionParentIndex.add(collectionPath);
- return PersistencePromise.resolve();
- }
- getCollectionParents(transaction, collectionId) {
- return PersistencePromise.resolve(this.collectionParentIndex.getEntries(collectionId));
- }
- addFieldIndex(transaction, index) {
- // Field indices are not supported with memory persistence.
- return PersistencePromise.resolve();
- }
- deleteFieldIndex(transaction, index) {
- // Field indices are not supported with memory persistence.
- return PersistencePromise.resolve();
- }
- getDocumentsMatchingTarget(transaction, target) {
- // Field indices are not supported with memory persistence.
- return PersistencePromise.resolve(null);
- }
- getIndexType(transaction, target) {
- // Field indices are not supported with memory persistence.
- return PersistencePromise.resolve(0 /* IndexType.NONE */);
- }
- getFieldIndexes(transaction, collectionGroup) {
- // Field indices are not supported with memory persistence.
- return PersistencePromise.resolve([]);
- }
- getNextCollectionGroupToUpdate(transaction) {
- // Field indices are not supported with memory persistence.
- return PersistencePromise.resolve(null);
- }
- getMinOffset(transaction, target) {
- return PersistencePromise.resolve(IndexOffset.min());
- }
- getMinOffsetFromCollectionGroup(transaction, collectionGroup) {
- return PersistencePromise.resolve(IndexOffset.min());
- }
- updateCollectionGroup(transaction, collectionGroup, offset) {
- // Field indices are not supported with memory persistence.
- return PersistencePromise.resolve();
- }
- updateIndexEntries(transaction, documents) {
- // Field indices are not supported with memory persistence.
- return PersistencePromise.resolve();
- }
- }
- /**
- * Internal implementation of the collection-parent index exposed by MemoryIndexManager.
- * Also used for in-memory caching by IndexedDbIndexManager and initial index population
- * in indexeddb_schema.ts
- */
- class MemoryCollectionParentIndex {
- constructor() {
- this.index = {};
- }
- // Returns false if the entry already existed.
- add(collectionPath) {
- const collectionId = collectionPath.lastSegment();
- const parentPath = collectionPath.popLast();
- const existingParents = this.index[collectionId] ||
- new SortedSet(ResourcePath.comparator);
- const added = !existingParents.has(parentPath);
- this.index[collectionId] = existingParents.add(parentPath);
- return added;
- }
- has(collectionPath) {
- const collectionId = collectionPath.lastSegment();
- const parentPath = collectionPath.popLast();
- const existingParents = this.index[collectionId];
- return existingParents && existingParents.has(parentPath);
- }
- getEntries(collectionId) {
- const parentPaths = this.index[collectionId] ||
- new SortedSet(ResourcePath.comparator);
- return parentPaths.toArray();
- }
- }
- /**
- * @license
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const LOG_TAG$f = 'IndexedDbIndexManager';
- const EMPTY_VALUE = new Uint8Array(0);
- /**
- * A persisted implementation of IndexManager.
- *
- * PORTING NOTE: Unlike iOS and Android, the Web SDK does not memoize index
- * data as it supports multi-tab access.
- */
- class IndexedDbIndexManager {
- constructor(user, databaseId) {
- this.user = user;
- this.databaseId = databaseId;
- /**
- * An in-memory copy of the index entries we've already written since the SDK
- * launched. Used to avoid re-writing the same entry repeatedly.
- *
- * This is *NOT* a complete cache of what's in persistence and so can never be
- * used to satisfy reads.
- */
- this.collectionParentsCache = new MemoryCollectionParentIndex();
- /**
- * Maps from a target to its equivalent list of sub-targets. Each sub-target
- * contains only one term from the target's disjunctive normal form (DNF).
- */
- this.targetToDnfSubTargets = new ObjectMap(t => canonifyTarget(t), (l, r) => targetEquals(l, r));
- this.uid = user.uid || '';
- }
- /**
- * Adds a new entry to the collection parent index.
- *
- * Repeated calls for the same collectionPath should be avoided within a
- * transaction as IndexedDbIndexManager only caches writes once a transaction
- * has been committed.
- */
- addToCollectionParentIndex(transaction, collectionPath) {
- if (!this.collectionParentsCache.has(collectionPath)) {
- const collectionId = collectionPath.lastSegment();
- const parentPath = collectionPath.popLast();
- transaction.addOnCommittedListener(() => {
- // Add the collection to the in memory cache only if the transaction was
- // successfully committed.
- this.collectionParentsCache.add(collectionPath);
- });
- const collectionParent = {
- collectionId,
- parent: encodeResourcePath(parentPath)
- };
- return collectionParentsStore(transaction).put(collectionParent);
- }
- return PersistencePromise.resolve();
- }
- getCollectionParents(transaction, collectionId) {
- const parentPaths = [];
- const range = IDBKeyRange.bound([collectionId, ''], [immediateSuccessor(collectionId), ''],
- /*lowerOpen=*/ false,
- /*upperOpen=*/ true);
- return collectionParentsStore(transaction)
- .loadAll(range)
- .next(entries => {
- for (const entry of entries) {
- // This collectionId guard shouldn't be necessary (and isn't as long
- // as we're running in a real browser), but there's a bug in
- // indexeddbshim that breaks our range in our tests running in node:
- // https://github.com/axemclion/IndexedDBShim/issues/334
- if (entry.collectionId !== collectionId) {
- break;
- }
- parentPaths.push(decodeResourcePath(entry.parent));
- }
- return parentPaths;
- });
- }
- addFieldIndex(transaction, index) {
- // TODO(indexing): Verify that the auto-incrementing index ID works in
- // Safari & Firefox.
- const indexes = indexConfigurationStore(transaction);
- const dbIndex = toDbIndexConfiguration(index);
- delete dbIndex.indexId; // `indexId` is auto-populated by IndexedDb
- const result = indexes.add(dbIndex);
- if (index.indexState) {
- const states = indexStateStore(transaction);
- return result.next(indexId => {
- states.put(toDbIndexState(indexId, this.user, index.indexState.sequenceNumber, index.indexState.offset));
- });
- }
- else {
- return result.next();
- }
- }
- deleteFieldIndex(transaction, index) {
- const indexes = indexConfigurationStore(transaction);
- const states = indexStateStore(transaction);
- const entries = indexEntriesStore(transaction);
- return indexes
- .delete(index.indexId)
- .next(() => states.delete(IDBKeyRange.bound([index.indexId], [index.indexId + 1],
- /*lowerOpen=*/ false,
- /*upperOpen=*/ true)))
- .next(() => entries.delete(IDBKeyRange.bound([index.indexId], [index.indexId + 1],
- /*lowerOpen=*/ false,
- /*upperOpen=*/ true)));
- }
- getDocumentsMatchingTarget(transaction, target) {
- const indexEntries = indexEntriesStore(transaction);
- let canServeTarget = true;
- const indexes = new Map();
- return PersistencePromise.forEach(this.getSubTargets(target), (subTarget) => {
- return this.getFieldIndex(transaction, subTarget).next(index => {
- canServeTarget && (canServeTarget = !!index);
- indexes.set(subTarget, index);
- });
- }).next(() => {
- if (!canServeTarget) {
- return PersistencePromise.resolve(null);
- }
- else {
- let existingKeys = documentKeySet();
- const result = [];
- return PersistencePromise.forEach(indexes, (index, subTarget) => {
- logDebug(LOG_TAG$f, `Using index ${fieldIndexToString(index)} to execute ${canonifyTarget(target)}`);
- const arrayValues = targetGetArrayValues(subTarget, index);
- const notInValues = targetGetNotInValues(subTarget, index);
- const lowerBound = targetGetLowerBound(subTarget, index);
- const upperBound = targetGetUpperBound(subTarget, index);
- const lowerBoundEncoded = this.encodeBound(index, subTarget, lowerBound);
- const upperBoundEncoded = this.encodeBound(index, subTarget, upperBound);
- const notInEncoded = this.encodeValues(index, subTarget, notInValues);
- const indexRanges = this.generateIndexRanges(index.indexId, arrayValues, lowerBoundEncoded, lowerBound.inclusive, upperBoundEncoded, upperBound.inclusive, notInEncoded);
- return PersistencePromise.forEach(indexRanges, (indexRange) => {
- return indexEntries
- .loadFirst(indexRange, target.limit)
- .next(entries => {
- entries.forEach(entry => {
- const documentKey = DocumentKey.fromSegments(entry.documentKey);
- if (!existingKeys.has(documentKey)) {
- existingKeys = existingKeys.add(documentKey);
- result.push(documentKey);
- }
- });
- });
- });
- }).next(() => result);
- }
- });
- }
- getSubTargets(target) {
- let subTargets = this.targetToDnfSubTargets.get(target);
- if (subTargets) {
- return subTargets;
- }
- if (target.filters.length === 0) {
- subTargets = [target];
- }
- else {
- // There is an implicit AND operation between all the filters stored in the target
- const dnf = getDnfTerms(CompositeFilter.create(target.filters, "and" /* CompositeOperator.AND */));
- subTargets = dnf.map(term => newTarget(target.path, target.collectionGroup, target.orderBy, term.getFilters(), target.limit, target.startAt, target.endAt));
- }
- this.targetToDnfSubTargets.set(target, subTargets);
- return subTargets;
- }
- /**
- * Constructs a key range query on `DbIndexEntryStore` that unions all
- * bounds.
- */
- generateIndexRanges(indexId, arrayValues, lowerBounds, lowerBoundInclusive, upperBounds, upperBoundInclusive, notInValues) {
- // The number of total index scans we union together. This is similar to a
- // distributed normal form, but adapted for array values. We create a single
- // index range per value in an ARRAY_CONTAINS or ARRAY_CONTAINS_ANY filter
- // combined with the values from the query bounds.
- const totalScans = (arrayValues != null ? arrayValues.length : 1) *
- Math.max(lowerBounds.length, upperBounds.length);
- const scansPerArrayElement = totalScans / (arrayValues != null ? arrayValues.length : 1);
- const indexRanges = [];
- for (let i = 0; i < totalScans; ++i) {
- const arrayValue = arrayValues
- ? this.encodeSingleElement(arrayValues[i / scansPerArrayElement])
- : EMPTY_VALUE;
- const lowerBound = this.generateLowerBound(indexId, arrayValue, lowerBounds[i % scansPerArrayElement], lowerBoundInclusive);
- const upperBound = this.generateUpperBound(indexId, arrayValue, upperBounds[i % scansPerArrayElement], upperBoundInclusive);
- const notInBound = notInValues.map(notIn => this.generateLowerBound(indexId, arrayValue, notIn,
- /* inclusive= */ true));
- indexRanges.push(...this.createRange(lowerBound, upperBound, notInBound));
- }
- return indexRanges;
- }
- /** Generates the lower bound for `arrayValue` and `directionalValue`. */
- generateLowerBound(indexId, arrayValue, directionalValue, inclusive) {
- const entry = new IndexEntry(indexId, DocumentKey.empty(), arrayValue, directionalValue);
- return inclusive ? entry : entry.successor();
- }
- /** Generates the upper bound for `arrayValue` and `directionalValue`. */
- generateUpperBound(indexId, arrayValue, directionalValue, inclusive) {
- const entry = new IndexEntry(indexId, DocumentKey.empty(), arrayValue, directionalValue);
- return inclusive ? entry.successor() : entry;
- }
- getFieldIndex(transaction, target) {
- const targetIndexMatcher = new TargetIndexMatcher(target);
- const collectionGroup = target.collectionGroup != null
- ? target.collectionGroup
- : target.path.lastSegment();
- return this.getFieldIndexes(transaction, collectionGroup).next(indexes => {
- // Return the index with the most number of segments.
- let index = null;
- for (const candidate of indexes) {
- const matches = targetIndexMatcher.servedByIndex(candidate);
- if (matches &&
- (!index || candidate.fields.length > index.fields.length)) {
- index = candidate;
- }
- }
- return index;
- });
- }
- getIndexType(transaction, target) {
- let indexType = 2 /* IndexType.FULL */;
- const subTargets = this.getSubTargets(target);
- return PersistencePromise.forEach(subTargets, (target) => {
- return this.getFieldIndex(transaction, target).next(index => {
- if (!index) {
- indexType = 0 /* IndexType.NONE */;
- }
- else if (indexType !== 0 /* IndexType.NONE */ &&
- index.fields.length < targetGetSegmentCount(target)) {
- indexType = 1 /* IndexType.PARTIAL */;
- }
- });
- }).next(() => {
- // OR queries have more than one sub-target (one sub-target per DNF term). We currently consider
- // OR queries that have a `limit` to have a partial index. For such queries we perform sorting
- // and apply the limit in memory as a post-processing step.
- if (targetHasLimit(target) &&
- subTargets.length > 1 &&
- indexType === 2 /* IndexType.FULL */) {
- return 1 /* IndexType.PARTIAL */;
- }
- return indexType;
- });
- }
- /**
- * Returns the byte encoded form of the directional values in the field index.
- * Returns `null` if the document does not have all fields specified in the
- * index.
- */
- encodeDirectionalElements(fieldIndex, document) {
- const encoder = new IndexByteEncoder();
- for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
- const field = document.data.field(segment.fieldPath);
- if (field == null) {
- return null;
- }
- const directionalEncoder = encoder.forKind(segment.kind);
- FirestoreIndexValueWriter.INSTANCE.writeIndexValue(field, directionalEncoder);
- }
- return encoder.encodedBytes();
- }
- /** Encodes a single value to the ascending index format. */
- encodeSingleElement(value) {
- const encoder = new IndexByteEncoder();
- FirestoreIndexValueWriter.INSTANCE.writeIndexValue(value, encoder.forKind(0 /* IndexKind.ASCENDING */));
- return encoder.encodedBytes();
- }
- /**
- * Returns an encoded form of the document key that sorts based on the key
- * ordering of the field index.
- */
- encodeDirectionalKey(fieldIndex, documentKey) {
- const encoder = new IndexByteEncoder();
- FirestoreIndexValueWriter.INSTANCE.writeIndexValue(refValue(this.databaseId, documentKey), encoder.forKind(fieldIndexGetKeyOrder(fieldIndex)));
- return encoder.encodedBytes();
- }
- /**
- * Encodes the given field values according to the specification in `target`.
- * For IN queries, a list of possible values is returned.
- */
- encodeValues(fieldIndex, target, values) {
- if (values === null) {
- return [];
- }
- let encoders = [];
- encoders.push(new IndexByteEncoder());
- let valueIdx = 0;
- for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
- const value = values[valueIdx++];
- for (const encoder of encoders) {
- if (this.isInFilter(target, segment.fieldPath) && isArray(value)) {
- encoders = this.expandIndexValues(encoders, segment, value);
- }
- else {
- const directionalEncoder = encoder.forKind(segment.kind);
- FirestoreIndexValueWriter.INSTANCE.writeIndexValue(value, directionalEncoder);
- }
- }
- }
- return this.getEncodedBytes(encoders);
- }
- /**
- * Encodes the given bounds according to the specification in `target`. For IN
- * queries, a list of possible values is returned.
- */
- encodeBound(fieldIndex, target, bound) {
- return this.encodeValues(fieldIndex, target, bound.position);
- }
- /** Returns the byte representation for the provided encoders. */
- getEncodedBytes(encoders) {
- const result = [];
- for (let i = 0; i < encoders.length; ++i) {
- result[i] = encoders[i].encodedBytes();
- }
- return result;
- }
- /**
- * Creates a separate encoder for each element of an array.
- *
- * The method appends each value to all existing encoders (e.g. filter("a",
- * "==", "a1").filter("b", "in", ["b1", "b2"]) becomes ["a1,b1", "a1,b2"]). A
- * list of new encoders is returned.
- */
- expandIndexValues(encoders, segment, value) {
- const prefixes = [...encoders];
- const results = [];
- for (const arrayElement of value.arrayValue.values || []) {
- for (const prefix of prefixes) {
- const clonedEncoder = new IndexByteEncoder();
- clonedEncoder.seed(prefix.encodedBytes());
- FirestoreIndexValueWriter.INSTANCE.writeIndexValue(arrayElement, clonedEncoder.forKind(segment.kind));
- results.push(clonedEncoder);
- }
- }
- return results;
- }
- isInFilter(target, fieldPath) {
- return !!target.filters.find(f => f instanceof FieldFilter &&
- f.field.isEqual(fieldPath) &&
- (f.op === "in" /* Operator.IN */ || f.op === "not-in" /* Operator.NOT_IN */));
- }
- getFieldIndexes(transaction, collectionGroup) {
- const indexes = indexConfigurationStore(transaction);
- const states = indexStateStore(transaction);
- return (collectionGroup
- ? indexes.loadAll(DbIndexConfigurationCollectionGroupIndex, IDBKeyRange.bound(collectionGroup, collectionGroup))
- : indexes.loadAll()).next(indexConfigs => {
- const result = [];
- return PersistencePromise.forEach(indexConfigs, (indexConfig) => {
- return states
- .get([indexConfig.indexId, this.uid])
- .next(indexState => {
- result.push(fromDbIndexConfiguration(indexConfig, indexState));
- });
- }).next(() => result);
- });
- }
- getNextCollectionGroupToUpdate(transaction) {
- return this.getFieldIndexes(transaction).next(indexes => {
- if (indexes.length === 0) {
- return null;
- }
- indexes.sort((l, r) => {
- const cmp = l.indexState.sequenceNumber - r.indexState.sequenceNumber;
- return cmp !== 0
- ? cmp
- : primitiveComparator(l.collectionGroup, r.collectionGroup);
- });
- return indexes[0].collectionGroup;
- });
- }
- updateCollectionGroup(transaction, collectionGroup, offset) {
- const indexes = indexConfigurationStore(transaction);
- const states = indexStateStore(transaction);
- return this.getNextSequenceNumber(transaction).next(nextSequenceNumber => indexes
- .loadAll(DbIndexConfigurationCollectionGroupIndex, IDBKeyRange.bound(collectionGroup, collectionGroup))
- .next(configs => PersistencePromise.forEach(configs, (config) => states.put(toDbIndexState(config.indexId, this.user, nextSequenceNumber, offset)))));
- }
- updateIndexEntries(transaction, documents) {
- // Porting Note: `getFieldIndexes()` on Web does not cache index lookups as
- // it could be used across different IndexedDB transactions. As any cached
- // data might be invalidated by other multi-tab clients, we can only trust
- // data within a single IndexedDB transaction. We therefore add a cache
- // here.
- const memoizedIndexes = new Map();
- return PersistencePromise.forEach(documents, (key, doc) => {
- const memoizedCollectionIndexes = memoizedIndexes.get(key.collectionGroup);
- const fieldIndexes = memoizedCollectionIndexes
- ? PersistencePromise.resolve(memoizedCollectionIndexes)
- : this.getFieldIndexes(transaction, key.collectionGroup);
- return fieldIndexes.next(fieldIndexes => {
- memoizedIndexes.set(key.collectionGroup, fieldIndexes);
- return PersistencePromise.forEach(fieldIndexes, (fieldIndex) => {
- return this.getExistingIndexEntries(transaction, key, fieldIndex).next(existingEntries => {
- const newEntries = this.computeIndexEntries(doc, fieldIndex);
- if (!existingEntries.isEqual(newEntries)) {
- return this.updateEntries(transaction, doc, fieldIndex, existingEntries, newEntries);
- }
- return PersistencePromise.resolve();
- });
- });
- });
- });
- }
- addIndexEntry(transaction, document, fieldIndex, indexEntry) {
- const indexEntries = indexEntriesStore(transaction);
- return indexEntries.put({
- indexId: indexEntry.indexId,
- uid: this.uid,
- arrayValue: indexEntry.arrayValue,
- directionalValue: indexEntry.directionalValue,
- orderedDocumentKey: this.encodeDirectionalKey(fieldIndex, document.key),
- documentKey: document.key.path.toArray()
- });
- }
- deleteIndexEntry(transaction, document, fieldIndex, indexEntry) {
- const indexEntries = indexEntriesStore(transaction);
- return indexEntries.delete([
- indexEntry.indexId,
- this.uid,
- indexEntry.arrayValue,
- indexEntry.directionalValue,
- this.encodeDirectionalKey(fieldIndex, document.key),
- document.key.path.toArray()
- ]);
- }
- getExistingIndexEntries(transaction, documentKey, fieldIndex) {
- const indexEntries = indexEntriesStore(transaction);
- let results = new SortedSet(indexEntryComparator);
- return indexEntries
- .iterate({
- index: DbIndexEntryDocumentKeyIndex,
- range: IDBKeyRange.only([
- fieldIndex.indexId,
- this.uid,
- this.encodeDirectionalKey(fieldIndex, documentKey)
- ])
- }, (_, entry) => {
- results = results.add(new IndexEntry(fieldIndex.indexId, documentKey, entry.arrayValue, entry.directionalValue));
- })
- .next(() => results);
- }
- /** Creates the index entries for the given document. */
- computeIndexEntries(document, fieldIndex) {
- let results = new SortedSet(indexEntryComparator);
- const directionalValue = this.encodeDirectionalElements(fieldIndex, document);
- if (directionalValue == null) {
- return results;
- }
- const arraySegment = fieldIndexGetArraySegment(fieldIndex);
- if (arraySegment != null) {
- const value = document.data.field(arraySegment.fieldPath);
- if (isArray(value)) {
- for (const arrayValue of value.arrayValue.values || []) {
- results = results.add(new IndexEntry(fieldIndex.indexId, document.key, this.encodeSingleElement(arrayValue), directionalValue));
- }
- }
- }
- else {
- results = results.add(new IndexEntry(fieldIndex.indexId, document.key, EMPTY_VALUE, directionalValue));
- }
- return results;
- }
- /**
- * Updates the index entries for the provided document by deleting entries
- * that are no longer referenced in `newEntries` and adding all newly added
- * entries.
- */
- updateEntries(transaction, document, fieldIndex, existingEntries, newEntries) {
- logDebug(LOG_TAG$f, "Updating index entries for document '%s'", document.key);
- const promises = [];
- diffSortedSets(existingEntries, newEntries, indexEntryComparator,
- /* onAdd= */ entry => {
- promises.push(this.addIndexEntry(transaction, document, fieldIndex, entry));
- },
- /* onRemove= */ entry => {
- promises.push(this.deleteIndexEntry(transaction, document, fieldIndex, entry));
- });
- return PersistencePromise.waitFor(promises);
- }
- getNextSequenceNumber(transaction) {
- let nextSequenceNumber = 1;
- const states = indexStateStore(transaction);
- return states
- .iterate({
- index: DbIndexStateSequenceNumberIndex,
- reverse: true,
- range: IDBKeyRange.upperBound([this.uid, Number.MAX_SAFE_INTEGER])
- }, (_, state, controller) => {
- controller.done();
- nextSequenceNumber = state.sequenceNumber + 1;
- })
- .next(() => nextSequenceNumber);
- }
- /**
- * Returns a new set of IDB ranges that splits the existing range and excludes
- * any values that match the `notInValue` from these ranges. As an example,
- * '[foo > 2 && foo != 3]` becomes `[foo > 2 && < 3, foo > 3]`.
- */
- createRange(lower, upper, notInValues) {
- // The notIn values need to be sorted and unique so that we can return a
- // sorted set of non-overlapping ranges.
- notInValues = notInValues
- .sort((l, r) => indexEntryComparator(l, r))
- .filter((el, i, values) => !i || indexEntryComparator(el, values[i - 1]) !== 0);
- const bounds = [];
- bounds.push(lower);
- for (const notInValue of notInValues) {
- const cmpToLower = indexEntryComparator(notInValue, lower);
- const cmpToUpper = indexEntryComparator(notInValue, upper);
- if (cmpToLower === 0) {
- // `notInValue` is the lower bound. We therefore need to raise the bound
- // to the next value.
- bounds[0] = lower.successor();
- }
- else if (cmpToLower > 0 && cmpToUpper < 0) {
- // `notInValue` is in the middle of the range
- bounds.push(notInValue);
- bounds.push(notInValue.successor());
- }
- else if (cmpToUpper > 0) {
- // `notInValue` (and all following values) are out of the range
- break;
- }
- }
- bounds.push(upper);
- const ranges = [];
- for (let i = 0; i < bounds.length; i += 2) {
- // If we encounter two bounds that will create an unmatchable key range,
- // then we return an empty set of key ranges.
- if (this.isRangeMatchable(bounds[i], bounds[i + 1])) {
- return [];
- }
- const lowerBound = [
- bounds[i].indexId,
- this.uid,
- bounds[i].arrayValue,
- bounds[i].directionalValue,
- EMPTY_VALUE,
- []
- ];
- const upperBound = [
- bounds[i + 1].indexId,
- this.uid,
- bounds[i + 1].arrayValue,
- bounds[i + 1].directionalValue,
- EMPTY_VALUE,
- []
- ];
- ranges.push(IDBKeyRange.bound(lowerBound, upperBound));
- }
- return ranges;
- }
- isRangeMatchable(lowerBound, upperBound) {
- // If lower bound is greater than the upper bound, then the key
- // range can never be matched.
- return indexEntryComparator(lowerBound, upperBound) > 0;
- }
- getMinOffsetFromCollectionGroup(transaction, collectionGroup) {
- return this.getFieldIndexes(transaction, collectionGroup).next(getMinOffsetFromFieldIndexes);
- }
- getMinOffset(transaction, target) {
- return PersistencePromise.mapArray(this.getSubTargets(target), (subTarget) => this.getFieldIndex(transaction, subTarget).next(index => index ? index : fail())).next(getMinOffsetFromFieldIndexes);
- }
- }
- /**
- * Helper to get a typed SimpleDbStore for the collectionParents
- * document store.
- */
- function collectionParentsStore(txn) {
- return getStore(txn, DbCollectionParentStore);
- }
- /**
- * Helper to get a typed SimpleDbStore for the index entry object store.
- */
- function indexEntriesStore(txn) {
- return getStore(txn, DbIndexEntryStore);
- }
- /**
- * Helper to get a typed SimpleDbStore for the index configuration object store.
- */
- function indexConfigurationStore(txn) {
- return getStore(txn, DbIndexConfigurationStore);
- }
- /**
- * Helper to get a typed SimpleDbStore for the index state object store.
- */
- function indexStateStore(txn) {
- return getStore(txn, DbIndexStateStore);
- }
- function getMinOffsetFromFieldIndexes(fieldIndexes) {
- hardAssert(fieldIndexes.length !== 0);
- let minOffset = fieldIndexes[0].indexState.offset;
- let maxBatchId = minOffset.largestBatchId;
- for (let i = 1; i < fieldIndexes.length; i++) {
- const newOffset = fieldIndexes[i].indexState.offset;
- if (indexOffsetComparator(newOffset, minOffset) < 0) {
- minOffset = newOffset;
- }
- if (maxBatchId < newOffset.largestBatchId) {
- maxBatchId = newOffset.largestBatchId;
- }
- }
- return new IndexOffset(minOffset.readTime, minOffset.documentKey, maxBatchId);
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Delete a mutation batch and the associated document mutations.
- * @returns A PersistencePromise of the document mutations that were removed.
- */
- function removeMutationBatch(txn, userId, batch) {
- const mutationStore = txn.store(DbMutationBatchStore);
- const indexTxn = txn.store(DbDocumentMutationStore);
- const promises = [];
- const range = IDBKeyRange.only(batch.batchId);
- let numDeleted = 0;
- const removePromise = mutationStore.iterate({ range }, (key, value, control) => {
- numDeleted++;
- return control.delete();
- });
- promises.push(removePromise.next(() => {
- hardAssert(numDeleted === 1);
- }));
- const removedDocuments = [];
- for (const mutation of batch.mutations) {
- const indexKey = newDbDocumentMutationKey(userId, mutation.key.path, batch.batchId);
- promises.push(indexTxn.delete(indexKey));
- removedDocuments.push(mutation.key);
- }
- return PersistencePromise.waitFor(promises).next(() => removedDocuments);
- }
- /**
- * Returns an approximate size for the given document.
- */
- function dbDocumentSize(doc) {
- if (!doc) {
- return 0;
- }
- let value;
- if (doc.document) {
- value = doc.document;
- }
- else if (doc.unknownDocument) {
- value = doc.unknownDocument;
- }
- else if (doc.noDocument) {
- value = doc.noDocument;
- }
- else {
- throw fail();
- }
- return JSON.stringify(value).length;
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /** A mutation queue for a specific user, backed by IndexedDB. */
- class IndexedDbMutationQueue {
- constructor(
- /**
- * The normalized userId (e.g. null UID => "" userId) used to store /
- * retrieve mutations.
- */
- userId, serializer, indexManager, referenceDelegate) {
- this.userId = userId;
- this.serializer = serializer;
- this.indexManager = indexManager;
- this.referenceDelegate = referenceDelegate;
- /**
- * Caches the document keys for pending mutation batches. If the mutation
- * has been removed from IndexedDb, the cached value may continue to
- * be used to retrieve the batch's document keys. To remove a cached value
- * locally, `removeCachedMutationKeys()` should be invoked either directly
- * or through `removeMutationBatches()`.
- *
- * With multi-tab, when the primary client acknowledges or rejects a mutation,
- * this cache is used by secondary clients to invalidate the local
- * view of the documents that were previously affected by the mutation.
- */
- // PORTING NOTE: Multi-tab only.
- this.documentKeysByBatchId = {};
- }
- /**
- * Creates a new mutation queue for the given user.
- * @param user - The user for which to create a mutation queue.
- * @param serializer - The serializer to use when persisting to IndexedDb.
- */
- static forUser(user, serializer, indexManager, referenceDelegate) {
- // TODO(mcg): Figure out what constraints there are on userIDs
- // In particular, are there any reserved characters? are empty ids allowed?
- // For the moment store these together in the same mutations table assuming
- // that empty userIDs aren't allowed.
- hardAssert(user.uid !== '');
- const userId = user.isAuthenticated() ? user.uid : '';
- return new IndexedDbMutationQueue(userId, serializer, indexManager, referenceDelegate);
- }
- checkEmpty(transaction) {
- let empty = true;
- const range = IDBKeyRange.bound([this.userId, Number.NEGATIVE_INFINITY], [this.userId, Number.POSITIVE_INFINITY]);
- return mutationsStore(transaction)
- .iterate({ index: DbMutationBatchUserMutationsIndex, range }, (key, value, control) => {
- empty = false;
- control.done();
- })
- .next(() => empty);
- }
- addMutationBatch(transaction, localWriteTime, baseMutations, mutations) {
- const documentStore = documentMutationsStore(transaction);
- const mutationStore = mutationsStore(transaction);
- // The IndexedDb implementation in Chrome (and Firefox) does not handle
- // compound indices that include auto-generated keys correctly. To ensure
- // that the index entry is added correctly in all browsers, we perform two
- // writes: The first write is used to retrieve the next auto-generated Batch
- // ID, and the second write populates the index and stores the actual
- // mutation batch.
- // See: https://bugs.chromium.org/p/chromium/issues/detail?id=701972
- // We write an empty object to obtain key
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- return mutationStore.add({}).next(batchId => {
- hardAssert(typeof batchId === 'number');
- const batch = new MutationBatch(batchId, localWriteTime, baseMutations, mutations);
- const dbBatch = toDbMutationBatch(this.serializer, this.userId, batch);
- const promises = [];
- let collectionParents = new SortedSet((l, r) => primitiveComparator(l.canonicalString(), r.canonicalString()));
- for (const mutation of mutations) {
- const indexKey = newDbDocumentMutationKey(this.userId, mutation.key.path, batchId);
- collectionParents = collectionParents.add(mutation.key.path.popLast());
- promises.push(mutationStore.put(dbBatch));
- promises.push(documentStore.put(indexKey, DbDocumentMutationPlaceholder));
- }
- collectionParents.forEach(parent => {
- promises.push(this.indexManager.addToCollectionParentIndex(transaction, parent));
- });
- transaction.addOnCommittedListener(() => {
- this.documentKeysByBatchId[batchId] = batch.keys();
- });
- return PersistencePromise.waitFor(promises).next(() => batch);
- });
- }
- lookupMutationBatch(transaction, batchId) {
- return mutationsStore(transaction)
- .get(batchId)
- .next(dbBatch => {
- if (dbBatch) {
- hardAssert(dbBatch.userId === this.userId);
- return fromDbMutationBatch(this.serializer, dbBatch);
- }
- return null;
- });
- }
- /**
- * Returns the document keys for the mutation batch with the given batchId.
- * For primary clients, this method returns `null` after
- * `removeMutationBatches()` has been called. Secondary clients return a
- * cached result until `removeCachedMutationKeys()` is invoked.
- */
- // PORTING NOTE: Multi-tab only.
- lookupMutationKeys(transaction, batchId) {
- if (this.documentKeysByBatchId[batchId]) {
- return PersistencePromise.resolve(this.documentKeysByBatchId[batchId]);
- }
- else {
- return this.lookupMutationBatch(transaction, batchId).next(batch => {
- if (batch) {
- const keys = batch.keys();
- this.documentKeysByBatchId[batchId] = keys;
- return keys;
- }
- else {
- return null;
- }
- });
- }
- }
- getNextMutationBatchAfterBatchId(transaction, batchId) {
- const nextBatchId = batchId + 1;
- const range = IDBKeyRange.lowerBound([this.userId, nextBatchId]);
- let foundBatch = null;
- return mutationsStore(transaction)
- .iterate({ index: DbMutationBatchUserMutationsIndex, range }, (key, dbBatch, control) => {
- if (dbBatch.userId === this.userId) {
- hardAssert(dbBatch.batchId >= nextBatchId);
- foundBatch = fromDbMutationBatch(this.serializer, dbBatch);
- }
- control.done();
- })
- .next(() => foundBatch);
- }
- getHighestUnacknowledgedBatchId(transaction) {
- const range = IDBKeyRange.upperBound([
- this.userId,
- Number.POSITIVE_INFINITY
- ]);
- let batchId = BATCHID_UNKNOWN;
- return mutationsStore(transaction)
- .iterate({ index: DbMutationBatchUserMutationsIndex, range, reverse: true }, (key, dbBatch, control) => {
- batchId = dbBatch.batchId;
- control.done();
- })
- .next(() => batchId);
- }
- getAllMutationBatches(transaction) {
- const range = IDBKeyRange.bound([this.userId, BATCHID_UNKNOWN], [this.userId, Number.POSITIVE_INFINITY]);
- return mutationsStore(transaction)
- .loadAll(DbMutationBatchUserMutationsIndex, range)
- .next(dbBatches => dbBatches.map(dbBatch => fromDbMutationBatch(this.serializer, dbBatch)));
- }
- getAllMutationBatchesAffectingDocumentKey(transaction, documentKey) {
- // Scan the document-mutation index starting with a prefix starting with
- // the given documentKey.
- const indexPrefix = newDbDocumentMutationPrefixForPath(this.userId, documentKey.path);
- const indexStart = IDBKeyRange.lowerBound(indexPrefix);
- const results = [];
- return documentMutationsStore(transaction)
- .iterate({ range: indexStart }, (indexKey, _, control) => {
- const [userID, encodedPath, batchId] = indexKey;
- // Only consider rows matching exactly the specific key of
- // interest. Note that because we order by path first, and we
- // order terminators before path separators, we'll encounter all
- // the index rows for documentKey contiguously. In particular, all
- // the rows for documentKey will occur before any rows for
- // documents nested in a subcollection beneath documentKey so we
- // can stop as soon as we hit any such row.
- const path = decodeResourcePath(encodedPath);
- if (userID !== this.userId || !documentKey.path.isEqual(path)) {
- control.done();
- return;
- }
- // Look up the mutation batch in the store.
- return mutationsStore(transaction)
- .get(batchId)
- .next(mutation => {
- if (!mutation) {
- throw fail();
- }
- hardAssert(mutation.userId === this.userId);
- results.push(fromDbMutationBatch(this.serializer, mutation));
- });
- })
- .next(() => results);
- }
- getAllMutationBatchesAffectingDocumentKeys(transaction, documentKeys) {
- let uniqueBatchIDs = new SortedSet(primitiveComparator);
- const promises = [];
- documentKeys.forEach(documentKey => {
- const indexStart = newDbDocumentMutationPrefixForPath(this.userId, documentKey.path);
- const range = IDBKeyRange.lowerBound(indexStart);
- const promise = documentMutationsStore(transaction).iterate({ range }, (indexKey, _, control) => {
- const [userID, encodedPath, batchID] = indexKey;
- // Only consider rows matching exactly the specific key of
- // interest. Note that because we order by path first, and we
- // order terminators before path separators, we'll encounter all
- // the index rows for documentKey contiguously. In particular, all
- // the rows for documentKey will occur before any rows for
- // documents nested in a subcollection beneath documentKey so we
- // can stop as soon as we hit any such row.
- const path = decodeResourcePath(encodedPath);
- if (userID !== this.userId || !documentKey.path.isEqual(path)) {
- control.done();
- return;
- }
- uniqueBatchIDs = uniqueBatchIDs.add(batchID);
- });
- promises.push(promise);
- });
- return PersistencePromise.waitFor(promises).next(() => this.lookupMutationBatches(transaction, uniqueBatchIDs));
- }
- getAllMutationBatchesAffectingQuery(transaction, query) {
- const queryPath = query.path;
- const immediateChildrenLength = queryPath.length + 1;
- // TODO(mcg): Actually implement a single-collection query
- //
- // This is actually executing an ancestor query, traversing the whole
- // subtree below the collection which can be horrifically inefficient for
- // some structures. The right way to solve this is to implement the full
- // value index, but that's not in the cards in the near future so this is
- // the best we can do for the moment.
- //
- // Since we don't yet index the actual properties in the mutations, our
- // current approach is to just return all mutation batches that affect
- // documents in the collection being queried.
- const indexPrefix = newDbDocumentMutationPrefixForPath(this.userId, queryPath);
- const indexStart = IDBKeyRange.lowerBound(indexPrefix);
- // Collect up unique batchIDs encountered during a scan of the index. Use a
- // SortedSet to accumulate batch IDs so they can be traversed in order in a
- // scan of the main table.
- let uniqueBatchIDs = new SortedSet(primitiveComparator);
- return documentMutationsStore(transaction)
- .iterate({ range: indexStart }, (indexKey, _, control) => {
- const [userID, encodedPath, batchID] = indexKey;
- const path = decodeResourcePath(encodedPath);
- if (userID !== this.userId || !queryPath.isPrefixOf(path)) {
- control.done();
- return;
- }
- // Rows with document keys more than one segment longer than the
- // query path can't be matches. For example, a query on 'rooms'
- // can't match the document /rooms/abc/messages/xyx.
- // TODO(mcg): we'll need a different scanner when we implement
- // ancestor queries.
- if (path.length !== immediateChildrenLength) {
- return;
- }
- uniqueBatchIDs = uniqueBatchIDs.add(batchID);
- })
- .next(() => this.lookupMutationBatches(transaction, uniqueBatchIDs));
- }
- lookupMutationBatches(transaction, batchIDs) {
- const results = [];
- const promises = [];
- // TODO(rockwood): Implement this using iterate.
- batchIDs.forEach(batchId => {
- promises.push(mutationsStore(transaction)
- .get(batchId)
- .next(mutation => {
- if (mutation === null) {
- throw fail();
- }
- hardAssert(mutation.userId === this.userId);
- results.push(fromDbMutationBatch(this.serializer, mutation));
- }));
- });
- return PersistencePromise.waitFor(promises).next(() => results);
- }
- removeMutationBatch(transaction, batch) {
- return removeMutationBatch(transaction.simpleDbTransaction, this.userId, batch).next(removedDocuments => {
- transaction.addOnCommittedListener(() => {
- this.removeCachedMutationKeys(batch.batchId);
- });
- return PersistencePromise.forEach(removedDocuments, (key) => {
- return this.referenceDelegate.markPotentiallyOrphaned(transaction, key);
- });
- });
- }
- /**
- * Clears the cached keys for a mutation batch. This method should be
- * called by secondary clients after they process mutation updates.
- *
- * Note that this method does not have to be called from primary clients as
- * the corresponding cache entries are cleared when an acknowledged or
- * rejected batch is removed from the mutation queue.
- */
- // PORTING NOTE: Multi-tab only
- removeCachedMutationKeys(batchId) {
- delete this.documentKeysByBatchId[batchId];
- }
- performConsistencyCheck(txn) {
- return this.checkEmpty(txn).next(empty => {
- if (!empty) {
- return PersistencePromise.resolve();
- }
- // Verify that there are no entries in the documentMutations index if
- // the queue is empty.
- const startRange = IDBKeyRange.lowerBound(newDbDocumentMutationPrefixForUser(this.userId));
- const danglingMutationReferences = [];
- return documentMutationsStore(txn)
- .iterate({ range: startRange }, (key, _, control) => {
- const userID = key[0];
- if (userID !== this.userId) {
- control.done();
- return;
- }
- else {
- const path = decodeResourcePath(key[1]);
- danglingMutationReferences.push(path);
- }
- })
- .next(() => {
- hardAssert(danglingMutationReferences.length === 0);
- });
- });
- }
- containsKey(txn, key) {
- return mutationQueueContainsKey(txn, this.userId, key);
- }
- // PORTING NOTE: Multi-tab only (state is held in memory in other clients).
- /** Returns the mutation queue's metadata from IndexedDb. */
- getMutationQueueMetadata(transaction) {
- return mutationQueuesStore(transaction)
- .get(this.userId)
- .next((metadata) => {
- return (metadata || {
- userId: this.userId,
- lastAcknowledgedBatchId: BATCHID_UNKNOWN,
- lastStreamToken: ''
- });
- });
- }
- }
- /**
- * @returns true if the mutation queue for the given user contains a pending
- * mutation for the given key.
- */
- function mutationQueueContainsKey(txn, userId, key) {
- const indexKey = newDbDocumentMutationPrefixForPath(userId, key.path);
- const encodedPath = indexKey[1];
- const startRange = IDBKeyRange.lowerBound(indexKey);
- let containsKey = false;
- return documentMutationsStore(txn)
- .iterate({ range: startRange, keysOnly: true }, (key, value, control) => {
- const [userID, keyPath, /*batchID*/ _] = key;
- if (userID === userId && keyPath === encodedPath) {
- containsKey = true;
- }
- control.done();
- })
- .next(() => containsKey);
- }
- /** Returns true if any mutation queue contains the given document. */
- function mutationQueuesContainKey(txn, docKey) {
- let found = false;
- return mutationQueuesStore(txn)
- .iterateSerial(userId => {
- return mutationQueueContainsKey(txn, userId, docKey).next(containsKey => {
- if (containsKey) {
- found = true;
- }
- return PersistencePromise.resolve(!containsKey);
- });
- })
- .next(() => found);
- }
- /**
- * Helper to get a typed SimpleDbStore for the mutations object store.
- */
- function mutationsStore(txn) {
- return getStore(txn, DbMutationBatchStore);
- }
- /**
- * Helper to get a typed SimpleDbStore for the mutationQueues object store.
- */
- function documentMutationsStore(txn) {
- return getStore(txn, DbDocumentMutationStore);
- }
- /**
- * Helper to get a typed SimpleDbStore for the mutationQueues object store.
- */
- function mutationQueuesStore(txn) {
- return getStore(txn, DbMutationQueueStore);
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /** Offset to ensure non-overlapping target ids. */
- const OFFSET = 2;
- /**
- * Generates monotonically increasing target IDs for sending targets to the
- * watch stream.
- *
- * The client constructs two generators, one for the target cache, and one for
- * for the sync engine (to generate limbo documents targets). These
- * generators produce non-overlapping IDs (by using even and odd IDs
- * respectively).
- *
- * By separating the target ID space, the query cache can generate target IDs
- * that persist across client restarts, while sync engine can independently
- * generate in-memory target IDs that are transient and can be reused after a
- * restart.
- */
- class TargetIdGenerator {
- constructor(lastId) {
- this.lastId = lastId;
- }
- next() {
- this.lastId += OFFSET;
- return this.lastId;
- }
- static forTargetCache() {
- // The target cache generator must return '2' in its first call to `next()`
- // as there is no differentiation in the protocol layer between an unset
- // number and the number '0'. If we were to sent a target with target ID
- // '0', the backend would consider it unset and replace it with its own ID.
- return new TargetIdGenerator(2 - OFFSET);
- }
- static forSyncEngine() {
- // Sync engine assigns target IDs for limbo document detection.
- return new TargetIdGenerator(1 - OFFSET);
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- class IndexedDbTargetCache {
- constructor(referenceDelegate, serializer) {
- this.referenceDelegate = referenceDelegate;
- this.serializer = serializer;
- }
- // PORTING NOTE: We don't cache global metadata for the target cache, since
- // some of it (in particular `highestTargetId`) can be modified by secondary
- // tabs. We could perhaps be more granular (and e.g. still cache
- // `lastRemoteSnapshotVersion` in memory) but for simplicity we currently go
- // to IndexedDb whenever we need to read metadata. We can revisit if it turns
- // out to have a meaningful performance impact.
- allocateTargetId(transaction) {
- return this.retrieveMetadata(transaction).next(metadata => {
- const targetIdGenerator = new TargetIdGenerator(metadata.highestTargetId);
- metadata.highestTargetId = targetIdGenerator.next();
- return this.saveMetadata(transaction, metadata).next(() => metadata.highestTargetId);
- });
- }
- getLastRemoteSnapshotVersion(transaction) {
- return this.retrieveMetadata(transaction).next(metadata => {
- return SnapshotVersion.fromTimestamp(new Timestamp(metadata.lastRemoteSnapshotVersion.seconds, metadata.lastRemoteSnapshotVersion.nanoseconds));
- });
- }
- getHighestSequenceNumber(transaction) {
- return this.retrieveMetadata(transaction).next(targetGlobal => targetGlobal.highestListenSequenceNumber);
- }
- setTargetsMetadata(transaction, highestListenSequenceNumber, lastRemoteSnapshotVersion) {
- return this.retrieveMetadata(transaction).next(metadata => {
- metadata.highestListenSequenceNumber = highestListenSequenceNumber;
- if (lastRemoteSnapshotVersion) {
- metadata.lastRemoteSnapshotVersion =
- lastRemoteSnapshotVersion.toTimestamp();
- }
- if (highestListenSequenceNumber > metadata.highestListenSequenceNumber) {
- metadata.highestListenSequenceNumber = highestListenSequenceNumber;
- }
- return this.saveMetadata(transaction, metadata);
- });
- }
- addTargetData(transaction, targetData) {
- return this.saveTargetData(transaction, targetData).next(() => {
- return this.retrieveMetadata(transaction).next(metadata => {
- metadata.targetCount += 1;
- this.updateMetadataFromTargetData(targetData, metadata);
- return this.saveMetadata(transaction, metadata);
- });
- });
- }
- updateTargetData(transaction, targetData) {
- return this.saveTargetData(transaction, targetData);
- }
- removeTargetData(transaction, targetData) {
- return this.removeMatchingKeysForTargetId(transaction, targetData.targetId)
- .next(() => targetsStore(transaction).delete(targetData.targetId))
- .next(() => this.retrieveMetadata(transaction))
- .next(metadata => {
- hardAssert(metadata.targetCount > 0);
- metadata.targetCount -= 1;
- return this.saveMetadata(transaction, metadata);
- });
- }
- /**
- * Drops any targets with sequence number less than or equal to the upper bound, excepting those
- * present in `activeTargetIds`. Document associations for the removed targets are also removed.
- * Returns the number of targets removed.
- */
- removeTargets(txn, upperBound, activeTargetIds) {
- let count = 0;
- const promises = [];
- return targetsStore(txn)
- .iterate((key, value) => {
- const targetData = fromDbTarget(value);
- if (targetData.sequenceNumber <= upperBound &&
- activeTargetIds.get(targetData.targetId) === null) {
- count++;
- promises.push(this.removeTargetData(txn, targetData));
- }
- })
- .next(() => PersistencePromise.waitFor(promises))
- .next(() => count);
- }
- /**
- * Call provided function with each `TargetData` that we have cached.
- */
- forEachTarget(txn, f) {
- return targetsStore(txn).iterate((key, value) => {
- const targetData = fromDbTarget(value);
- f(targetData);
- });
- }
- retrieveMetadata(transaction) {
- return globalTargetStore(transaction)
- .get(DbTargetGlobalKey)
- .next(metadata => {
- hardAssert(metadata !== null);
- return metadata;
- });
- }
- saveMetadata(transaction, metadata) {
- return globalTargetStore(transaction).put(DbTargetGlobalKey, metadata);
- }
- saveTargetData(transaction, targetData) {
- return targetsStore(transaction).put(toDbTarget(this.serializer, targetData));
- }
- /**
- * In-place updates the provided metadata to account for values in the given
- * TargetData. Saving is done separately. Returns true if there were any
- * changes to the metadata.
- */
- updateMetadataFromTargetData(targetData, metadata) {
- let updated = false;
- if (targetData.targetId > metadata.highestTargetId) {
- metadata.highestTargetId = targetData.targetId;
- updated = true;
- }
- if (targetData.sequenceNumber > metadata.highestListenSequenceNumber) {
- metadata.highestListenSequenceNumber = targetData.sequenceNumber;
- updated = true;
- }
- return updated;
- }
- getTargetCount(transaction) {
- return this.retrieveMetadata(transaction).next(metadata => metadata.targetCount);
- }
- getTargetData(transaction, target) {
- // Iterating by the canonicalId may yield more than one result because
- // canonicalId values are not required to be unique per target. This query
- // depends on the queryTargets index to be efficient.
- const canonicalId = canonifyTarget(target);
- const range = IDBKeyRange.bound([canonicalId, Number.NEGATIVE_INFINITY], [canonicalId, Number.POSITIVE_INFINITY]);
- let result = null;
- return targetsStore(transaction)
- .iterate({ range, index: DbTargetQueryTargetsIndexName }, (key, value, control) => {
- const found = fromDbTarget(value);
- // After finding a potential match, check that the target is
- // actually equal to the requested target.
- if (targetEquals(target, found.target)) {
- result = found;
- control.done();
- }
- })
- .next(() => result);
- }
- addMatchingKeys(txn, keys, targetId) {
- // PORTING NOTE: The reverse index (documentsTargets) is maintained by
- // IndexedDb.
- const promises = [];
- const store = documentTargetStore(txn);
- keys.forEach(key => {
- const path = encodeResourcePath(key.path);
- promises.push(store.put({ targetId, path }));
- promises.push(this.referenceDelegate.addReference(txn, targetId, key));
- });
- return PersistencePromise.waitFor(promises);
- }
- removeMatchingKeys(txn, keys, targetId) {
- // PORTING NOTE: The reverse index (documentsTargets) is maintained by
- // IndexedDb.
- const store = documentTargetStore(txn);
- return PersistencePromise.forEach(keys, (key) => {
- const path = encodeResourcePath(key.path);
- return PersistencePromise.waitFor([
- store.delete([targetId, path]),
- this.referenceDelegate.removeReference(txn, targetId, key)
- ]);
- });
- }
- removeMatchingKeysForTargetId(txn, targetId) {
- const store = documentTargetStore(txn);
- const range = IDBKeyRange.bound([targetId], [targetId + 1],
- /*lowerOpen=*/ false,
- /*upperOpen=*/ true);
- return store.delete(range);
- }
- getMatchingKeysForTargetId(txn, targetId) {
- const range = IDBKeyRange.bound([targetId], [targetId + 1],
- /*lowerOpen=*/ false,
- /*upperOpen=*/ true);
- const store = documentTargetStore(txn);
- let result = documentKeySet();
- return store
- .iterate({ range, keysOnly: true }, (key, _, control) => {
- const path = decodeResourcePath(key[1]);
- const docKey = new DocumentKey(path);
- result = result.add(docKey);
- })
- .next(() => result);
- }
- containsKey(txn, key) {
- const path = encodeResourcePath(key.path);
- const range = IDBKeyRange.bound([path], [immediateSuccessor(path)],
- /*lowerOpen=*/ false,
- /*upperOpen=*/ true);
- let count = 0;
- return documentTargetStore(txn)
- .iterate({
- index: DbTargetDocumentDocumentTargetsIndex,
- keysOnly: true,
- range
- }, ([targetId, path], _, control) => {
- // Having a sentinel row for a document does not count as containing that document;
- // For the target cache, containing the document means the document is part of some
- // target.
- if (targetId !== 0) {
- count++;
- control.done();
- }
- })
- .next(() => count > 0);
- }
- /**
- * Looks up a TargetData entry by target ID.
- *
- * @param targetId - The target ID of the TargetData entry to look up.
- * @returns The cached TargetData entry, or null if the cache has no entry for
- * the target.
- */
- // PORTING NOTE: Multi-tab only.
- getTargetDataForTarget(transaction, targetId) {
- return targetsStore(transaction)
- .get(targetId)
- .next(found => {
- if (found) {
- return fromDbTarget(found);
- }
- else {
- return null;
- }
- });
- }
- }
- /**
- * Helper to get a typed SimpleDbStore for the queries object store.
- */
- function targetsStore(txn) {
- return getStore(txn, DbTargetStore);
- }
- /**
- * Helper to get a typed SimpleDbStore for the target globals object store.
- */
- function globalTargetStore(txn) {
- return getStore(txn, DbTargetGlobalStore);
- }
- /**
- * Helper to get a typed SimpleDbStore for the document target object store.
- */
- function documentTargetStore(txn) {
- return getStore(txn, DbTargetDocumentStore);
- }
- /**
- * @license
- * Copyright 2018 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const GC_DID_NOT_RUN = {
- didRun: false,
- sequenceNumbersCollected: 0,
- targetsRemoved: 0,
- documentsRemoved: 0
- };
- const LRU_COLLECTION_DISABLED = -1;
- const LRU_DEFAULT_CACHE_SIZE_BYTES = 40 * 1024 * 1024;
- class LruParams {
- constructor(
- // When we attempt to collect, we will only do so if the cache size is greater than this
- // threshold. Passing `COLLECTION_DISABLED` here will cause collection to always be skipped.
- cacheSizeCollectionThreshold,
- // The percentage of sequence numbers that we will attempt to collect
- percentileToCollect,
- // A cap on the total number of sequence numbers that will be collected. This prevents
- // us from collecting a huge number of sequence numbers if the cache has grown very large.
- maximumSequenceNumbersToCollect) {
- this.cacheSizeCollectionThreshold = cacheSizeCollectionThreshold;
- this.percentileToCollect = percentileToCollect;
- this.maximumSequenceNumbersToCollect = maximumSequenceNumbersToCollect;
- }
- static withCacheSize(cacheSize) {
- return new LruParams(cacheSize, LruParams.DEFAULT_COLLECTION_PERCENTILE, LruParams.DEFAULT_MAX_SEQUENCE_NUMBERS_TO_COLLECT);
- }
- }
- LruParams.DEFAULT_COLLECTION_PERCENTILE = 10;
- LruParams.DEFAULT_MAX_SEQUENCE_NUMBERS_TO_COLLECT = 1000;
- LruParams.DEFAULT = new LruParams(LRU_DEFAULT_CACHE_SIZE_BYTES, LruParams.DEFAULT_COLLECTION_PERCENTILE, LruParams.DEFAULT_MAX_SEQUENCE_NUMBERS_TO_COLLECT);
- LruParams.DISABLED = new LruParams(LRU_COLLECTION_DISABLED, 0, 0);
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const LOG_TAG$e = 'LruGarbageCollector';
- const LRU_MINIMUM_CACHE_SIZE_BYTES = 1 * 1024 * 1024;
- /** How long we wait to try running LRU GC after SDK initialization. */
- const INITIAL_GC_DELAY_MS = 1 * 60 * 1000;
- /** Minimum amount of time between GC checks, after the first one. */
- const REGULAR_GC_DELAY_MS = 5 * 60 * 1000;
- function bufferEntryComparator([aSequence, aIndex], [bSequence, bIndex]) {
- const seqCmp = primitiveComparator(aSequence, bSequence);
- if (seqCmp === 0) {
- // This order doesn't matter, but we can bias against churn by sorting
- // entries created earlier as less than newer entries.
- return primitiveComparator(aIndex, bIndex);
- }
- else {
- return seqCmp;
- }
- }
- /**
- * Used to calculate the nth sequence number. Keeps a rolling buffer of the
- * lowest n values passed to `addElement`, and finally reports the largest of
- * them in `maxValue`.
- */
- class RollingSequenceNumberBuffer {
- constructor(maxElements) {
- this.maxElements = maxElements;
- this.buffer = new SortedSet(bufferEntryComparator);
- this.previousIndex = 0;
- }
- nextIndex() {
- return ++this.previousIndex;
- }
- addElement(sequenceNumber) {
- const entry = [sequenceNumber, this.nextIndex()];
- if (this.buffer.size < this.maxElements) {
- this.buffer = this.buffer.add(entry);
- }
- else {
- const highestValue = this.buffer.last();
- if (bufferEntryComparator(entry, highestValue) < 0) {
- this.buffer = this.buffer.delete(highestValue).add(entry);
- }
- }
- }
- get maxValue() {
- // Guaranteed to be non-empty. If we decide we are not collecting any
- // sequence numbers, nthSequenceNumber below short-circuits. If we have
- // decided that we are collecting n sequence numbers, it's because n is some
- // percentage of the existing sequence numbers. That means we should never
- // be in a situation where we are collecting sequence numbers but don't
- // actually have any.
- return this.buffer.last()[0];
- }
- }
- /**
- * This class is responsible for the scheduling of LRU garbage collection. It handles checking
- * whether or not GC is enabled, as well as which delay to use before the next run.
- */
- class LruScheduler {
- constructor(garbageCollector, asyncQueue, localStore) {
- this.garbageCollector = garbageCollector;
- this.asyncQueue = asyncQueue;
- this.localStore = localStore;
- this.gcTask = null;
- }
- start() {
- if (this.garbageCollector.params.cacheSizeCollectionThreshold !==
- LRU_COLLECTION_DISABLED) {
- this.scheduleGC(INITIAL_GC_DELAY_MS);
- }
- }
- stop() {
- if (this.gcTask) {
- this.gcTask.cancel();
- this.gcTask = null;
- }
- }
- get started() {
- return this.gcTask !== null;
- }
- scheduleGC(delay) {
- logDebug(LOG_TAG$e, `Garbage collection scheduled in ${delay}ms`);
- this.gcTask = this.asyncQueue.enqueueAfterDelay("lru_garbage_collection" /* TimerId.LruGarbageCollection */, delay, async () => {
- this.gcTask = null;
- try {
- await this.localStore.collectGarbage(this.garbageCollector);
- }
- catch (e) {
- if (isIndexedDbTransactionError(e)) {
- logDebug(LOG_TAG$e, 'Ignoring IndexedDB error during garbage collection: ', e);
- }
- else {
- await ignoreIfPrimaryLeaseLoss(e);
- }
- }
- await this.scheduleGC(REGULAR_GC_DELAY_MS);
- });
- }
- }
- /**
- * Implements the steps for LRU garbage collection.
- */
- class LruGarbageCollectorImpl {
- constructor(delegate, params) {
- this.delegate = delegate;
- this.params = params;
- }
- calculateTargetCount(txn, percentile) {
- return this.delegate.getSequenceNumberCount(txn).next(targetCount => {
- return Math.floor((percentile / 100.0) * targetCount);
- });
- }
- nthSequenceNumber(txn, n) {
- if (n === 0) {
- return PersistencePromise.resolve(ListenSequence.INVALID);
- }
- const buffer = new RollingSequenceNumberBuffer(n);
- return this.delegate
- .forEachTarget(txn, target => buffer.addElement(target.sequenceNumber))
- .next(() => {
- return this.delegate.forEachOrphanedDocumentSequenceNumber(txn, sequenceNumber => buffer.addElement(sequenceNumber));
- })
- .next(() => buffer.maxValue);
- }
- removeTargets(txn, upperBound, activeTargetIds) {
- return this.delegate.removeTargets(txn, upperBound, activeTargetIds);
- }
- removeOrphanedDocuments(txn, upperBound) {
- return this.delegate.removeOrphanedDocuments(txn, upperBound);
- }
- collect(txn, activeTargetIds) {
- if (this.params.cacheSizeCollectionThreshold === LRU_COLLECTION_DISABLED) {
- logDebug('LruGarbageCollector', 'Garbage collection skipped; disabled');
- return PersistencePromise.resolve(GC_DID_NOT_RUN);
- }
- return this.getCacheSize(txn).next(cacheSize => {
- if (cacheSize < this.params.cacheSizeCollectionThreshold) {
- logDebug('LruGarbageCollector', `Garbage collection skipped; Cache size ${cacheSize} ` +
- `is lower than threshold ${this.params.cacheSizeCollectionThreshold}`);
- return GC_DID_NOT_RUN;
- }
- else {
- return this.runGarbageCollection(txn, activeTargetIds);
- }
- });
- }
- getCacheSize(txn) {
- return this.delegate.getCacheSize(txn);
- }
- runGarbageCollection(txn, activeTargetIds) {
- let upperBoundSequenceNumber;
- let sequenceNumbersToCollect, targetsRemoved;
- // Timestamps for various pieces of the process
- let countedTargetsTs, foundUpperBoundTs, removedTargetsTs, removedDocumentsTs;
- const startTs = Date.now();
- return this.calculateTargetCount(txn, this.params.percentileToCollect)
- .next(sequenceNumbers => {
- // Cap at the configured max
- if (sequenceNumbers > this.params.maximumSequenceNumbersToCollect) {
- logDebug('LruGarbageCollector', 'Capping sequence numbers to collect down ' +
- `to the maximum of ${this.params.maximumSequenceNumbersToCollect} ` +
- `from ${sequenceNumbers}`);
- sequenceNumbersToCollect =
- this.params.maximumSequenceNumbersToCollect;
- }
- else {
- sequenceNumbersToCollect = sequenceNumbers;
- }
- countedTargetsTs = Date.now();
- return this.nthSequenceNumber(txn, sequenceNumbersToCollect);
- })
- .next(upperBound => {
- upperBoundSequenceNumber = upperBound;
- foundUpperBoundTs = Date.now();
- return this.removeTargets(txn, upperBoundSequenceNumber, activeTargetIds);
- })
- .next(numTargetsRemoved => {
- targetsRemoved = numTargetsRemoved;
- removedTargetsTs = Date.now();
- return this.removeOrphanedDocuments(txn, upperBoundSequenceNumber);
- })
- .next(documentsRemoved => {
- removedDocumentsTs = Date.now();
- if (getLogLevel() <= logger.LogLevel.DEBUG) {
- const desc = 'LRU Garbage Collection\n' +
- `\tCounted targets in ${countedTargetsTs - startTs}ms\n` +
- `\tDetermined least recently used ${sequenceNumbersToCollect} in ` +
- `${foundUpperBoundTs - countedTargetsTs}ms\n` +
- `\tRemoved ${targetsRemoved} targets in ` +
- `${removedTargetsTs - foundUpperBoundTs}ms\n` +
- `\tRemoved ${documentsRemoved} documents in ` +
- `${removedDocumentsTs - removedTargetsTs}ms\n` +
- `Total Duration: ${removedDocumentsTs - startTs}ms`;
- logDebug('LruGarbageCollector', desc);
- }
- return PersistencePromise.resolve({
- didRun: true,
- sequenceNumbersCollected: sequenceNumbersToCollect,
- targetsRemoved,
- documentsRemoved
- });
- });
- }
- }
- function newLruGarbageCollector(delegate, params) {
- return new LruGarbageCollectorImpl(delegate, params);
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /** Provides LRU functionality for IndexedDB persistence. */
- class IndexedDbLruDelegateImpl {
- constructor(db, params) {
- this.db = db;
- this.garbageCollector = newLruGarbageCollector(this, params);
- }
- getSequenceNumberCount(txn) {
- const docCountPromise = this.orphanedDocumentCount(txn);
- const targetCountPromise = this.db.getTargetCache().getTargetCount(txn);
- return targetCountPromise.next(targetCount => docCountPromise.next(docCount => targetCount + docCount));
- }
- orphanedDocumentCount(txn) {
- let orphanedCount = 0;
- return this.forEachOrphanedDocumentSequenceNumber(txn, _ => {
- orphanedCount++;
- }).next(() => orphanedCount);
- }
- forEachTarget(txn, f) {
- return this.db.getTargetCache().forEachTarget(txn, f);
- }
- forEachOrphanedDocumentSequenceNumber(txn, f) {
- return this.forEachOrphanedDocument(txn, (docKey, sequenceNumber) => f(sequenceNumber));
- }
- addReference(txn, targetId, key) {
- return writeSentinelKey(txn, key);
- }
- removeReference(txn, targetId, key) {
- return writeSentinelKey(txn, key);
- }
- removeTargets(txn, upperBound, activeTargetIds) {
- return this.db.getTargetCache().removeTargets(txn, upperBound, activeTargetIds);
- }
- markPotentiallyOrphaned(txn, key) {
- return writeSentinelKey(txn, key);
- }
- /**
- * Returns true if anything would prevent this document from being garbage
- * collected, given that the document in question is not present in any
- * targets and has a sequence number less than or equal to the upper bound for
- * the collection run.
- */
- isPinned(txn, docKey) {
- return mutationQueuesContainKey(txn, docKey);
- }
- removeOrphanedDocuments(txn, upperBound) {
- const documentCache = this.db.getRemoteDocumentCache();
- const changeBuffer = documentCache.newChangeBuffer();
- const promises = [];
- let documentCount = 0;
- const iteration = this.forEachOrphanedDocument(txn, (docKey, sequenceNumber) => {
- if (sequenceNumber <= upperBound) {
- const p = this.isPinned(txn, docKey).next(isPinned => {
- if (!isPinned) {
- documentCount++;
- // Our size accounting requires us to read all documents before
- // removing them.
- return changeBuffer.getEntry(txn, docKey).next(() => {
- changeBuffer.removeEntry(docKey, SnapshotVersion.min());
- return documentTargetStore(txn).delete(sentinelKey$1(docKey));
- });
- }
- });
- promises.push(p);
- }
- });
- return iteration
- .next(() => PersistencePromise.waitFor(promises))
- .next(() => changeBuffer.apply(txn))
- .next(() => documentCount);
- }
- removeTarget(txn, targetData) {
- const updated = targetData.withSequenceNumber(txn.currentSequenceNumber);
- return this.db.getTargetCache().updateTargetData(txn, updated);
- }
- updateLimboDocument(txn, key) {
- return writeSentinelKey(txn, key);
- }
- /**
- * Call provided function for each document in the cache that is 'orphaned'. Orphaned
- * means not a part of any target, so the only entry in the target-document index for
- * that document will be the sentinel row (targetId 0), which will also have the sequence
- * number for the last time the document was accessed.
- */
- forEachOrphanedDocument(txn, f) {
- const store = documentTargetStore(txn);
- let nextToReport = ListenSequence.INVALID;
- let nextPath;
- return store
- .iterate({
- index: DbTargetDocumentDocumentTargetsIndex
- }, ([targetId, docKey], { path, sequenceNumber }) => {
- if (targetId === 0) {
- // if nextToReport is valid, report it, this is a new key so the
- // last one must not be a member of any targets.
- if (nextToReport !== ListenSequence.INVALID) {
- f(new DocumentKey(decodeResourcePath(nextPath)), nextToReport);
- }
- // set nextToReport to be this sequence number. It's the next one we
- // might report, if we don't find any targets for this document.
- // Note that the sequence number must be defined when the targetId
- // is 0.
- nextToReport = sequenceNumber;
- nextPath = path;
- }
- else {
- // set nextToReport to be invalid, we know we don't need to report
- // this one since we found a target for it.
- nextToReport = ListenSequence.INVALID;
- }
- })
- .next(() => {
- // Since we report sequence numbers after getting to the next key, we
- // need to check if the last key we iterated over was an orphaned
- // document and report it.
- if (nextToReport !== ListenSequence.INVALID) {
- f(new DocumentKey(decodeResourcePath(nextPath)), nextToReport);
- }
- });
- }
- getCacheSize(txn) {
- return this.db.getRemoteDocumentCache().getSize(txn);
- }
- }
- function sentinelKey$1(key) {
- return [0, encodeResourcePath(key.path)];
- }
- /**
- * @returns A value suitable for writing a sentinel row in the target-document
- * store.
- */
- function sentinelRow(key, sequenceNumber) {
- return { targetId: 0, path: encodeResourcePath(key.path), sequenceNumber };
- }
- function writeSentinelKey(txn, key) {
- return documentTargetStore(txn).put(sentinelRow(key, txn.currentSequenceNumber));
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * An in-memory buffer of entries to be written to a RemoteDocumentCache.
- * It can be used to batch up a set of changes to be written to the cache, but
- * additionally supports reading entries back with the `getEntry()` method,
- * falling back to the underlying RemoteDocumentCache if no entry is
- * buffered.
- *
- * Entries added to the cache *must* be read first. This is to facilitate
- * calculating the size delta of the pending changes.
- *
- * PORTING NOTE: This class was implemented then removed from other platforms.
- * If byte-counting ends up being needed on the other platforms, consider
- * porting this class as part of that implementation work.
- */
- class RemoteDocumentChangeBuffer {
- constructor() {
- // A mapping of document key to the new cache entry that should be written.
- this.changes = new ObjectMap(key => key.toString(), (l, r) => l.isEqual(r));
- this.changesApplied = false;
- }
- /**
- * Buffers a `RemoteDocumentCache.addEntry()` call.
- *
- * You can only modify documents that have already been retrieved via
- * `getEntry()/getEntries()` (enforced via IndexedDbs `apply()`).
- */
- addEntry(document) {
- this.assertNotApplied();
- this.changes.set(document.key, document);
- }
- /**
- * Buffers a `RemoteDocumentCache.removeEntry()` call.
- *
- * You can only remove documents that have already been retrieved via
- * `getEntry()/getEntries()` (enforced via IndexedDbs `apply()`).
- */
- removeEntry(key, readTime) {
- this.assertNotApplied();
- this.changes.set(key, MutableDocument.newInvalidDocument(key).setReadTime(readTime));
- }
- /**
- * Looks up an entry in the cache. The buffered changes will first be checked,
- * and if no buffered change applies, this will forward to
- * `RemoteDocumentCache.getEntry()`.
- *
- * @param transaction - The transaction in which to perform any persistence
- * operations.
- * @param documentKey - The key of the entry to look up.
- * @returns The cached document or an invalid document if we have nothing
- * cached.
- */
- getEntry(transaction, documentKey) {
- this.assertNotApplied();
- const bufferedEntry = this.changes.get(documentKey);
- if (bufferedEntry !== undefined) {
- return PersistencePromise.resolve(bufferedEntry);
- }
- else {
- return this.getFromCache(transaction, documentKey);
- }
- }
- /**
- * Looks up several entries in the cache, forwarding to
- * `RemoteDocumentCache.getEntry()`.
- *
- * @param transaction - The transaction in which to perform any persistence
- * operations.
- * @param documentKeys - The keys of the entries to look up.
- * @returns A map of cached documents, indexed by key. If an entry cannot be
- * found, the corresponding key will be mapped to an invalid document.
- */
- getEntries(transaction, documentKeys) {
- return this.getAllFromCache(transaction, documentKeys);
- }
- /**
- * Applies buffered changes to the underlying RemoteDocumentCache, using
- * the provided transaction.
- */
- apply(transaction) {
- this.assertNotApplied();
- this.changesApplied = true;
- return this.applyChanges(transaction);
- }
- /** Helper to assert this.changes is not null */
- assertNotApplied() {
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * The RemoteDocumentCache for IndexedDb. To construct, invoke
- * `newIndexedDbRemoteDocumentCache()`.
- */
- class IndexedDbRemoteDocumentCacheImpl {
- constructor(serializer) {
- this.serializer = serializer;
- }
- setIndexManager(indexManager) {
- this.indexManager = indexManager;
- }
- /**
- * Adds the supplied entries to the cache.
- *
- * All calls of `addEntry` are required to go through the RemoteDocumentChangeBuffer
- * returned by `newChangeBuffer()` to ensure proper accounting of metadata.
- */
- addEntry(transaction, key, doc) {
- const documentStore = remoteDocumentsStore(transaction);
- return documentStore.put(doc);
- }
- /**
- * Removes a document from the cache.
- *
- * All calls of `removeEntry` are required to go through the RemoteDocumentChangeBuffer
- * returned by `newChangeBuffer()` to ensure proper accounting of metadata.
- */
- removeEntry(transaction, documentKey, readTime) {
- const store = remoteDocumentsStore(transaction);
- return store.delete(dbReadTimeKey(documentKey, readTime));
- }
- /**
- * Updates the current cache size.
- *
- * Callers to `addEntry()` and `removeEntry()` *must* call this afterwards to update the
- * cache's metadata.
- */
- updateMetadata(transaction, sizeDelta) {
- return this.getMetadata(transaction).next(metadata => {
- metadata.byteSize += sizeDelta;
- return this.setMetadata(transaction, metadata);
- });
- }
- getEntry(transaction, documentKey) {
- let doc = MutableDocument.newInvalidDocument(documentKey);
- return remoteDocumentsStore(transaction)
- .iterate({
- index: DbRemoteDocumentDocumentKeyIndex,
- range: IDBKeyRange.only(dbKey(documentKey))
- }, (_, dbRemoteDoc) => {
- doc = this.maybeDecodeDocument(documentKey, dbRemoteDoc);
- })
- .next(() => doc);
- }
- /**
- * Looks up an entry in the cache.
- *
- * @param documentKey - The key of the entry to look up.
- * @returns The cached document entry and its size.
- */
- getSizedEntry(transaction, documentKey) {
- let result = {
- size: 0,
- document: MutableDocument.newInvalidDocument(documentKey)
- };
- return remoteDocumentsStore(transaction)
- .iterate({
- index: DbRemoteDocumentDocumentKeyIndex,
- range: IDBKeyRange.only(dbKey(documentKey))
- }, (_, dbRemoteDoc) => {
- result = {
- document: this.maybeDecodeDocument(documentKey, dbRemoteDoc),
- size: dbDocumentSize(dbRemoteDoc)
- };
- })
- .next(() => result);
- }
- getEntries(transaction, documentKeys) {
- let results = mutableDocumentMap();
- return this.forEachDbEntry(transaction, documentKeys, (key, dbRemoteDoc) => {
- const doc = this.maybeDecodeDocument(key, dbRemoteDoc);
- results = results.insert(key, doc);
- }).next(() => results);
- }
- /**
- * Looks up several entries in the cache.
- *
- * @param documentKeys - The set of keys entries to look up.
- * @returns A map of documents indexed by key and a map of sizes indexed by
- * key (zero if the document does not exist).
- */
- getSizedEntries(transaction, documentKeys) {
- let results = mutableDocumentMap();
- let sizeMap = new SortedMap(DocumentKey.comparator);
- return this.forEachDbEntry(transaction, documentKeys, (key, dbRemoteDoc) => {
- const doc = this.maybeDecodeDocument(key, dbRemoteDoc);
- results = results.insert(key, doc);
- sizeMap = sizeMap.insert(key, dbDocumentSize(dbRemoteDoc));
- }).next(() => {
- return { documents: results, sizeMap };
- });
- }
- forEachDbEntry(transaction, documentKeys, callback) {
- if (documentKeys.isEmpty()) {
- return PersistencePromise.resolve();
- }
- let sortedKeys = new SortedSet(dbKeyComparator);
- documentKeys.forEach(e => (sortedKeys = sortedKeys.add(e)));
- const range = IDBKeyRange.bound(dbKey(sortedKeys.first()), dbKey(sortedKeys.last()));
- const keyIter = sortedKeys.getIterator();
- let nextKey = keyIter.getNext();
- return remoteDocumentsStore(transaction)
- .iterate({ index: DbRemoteDocumentDocumentKeyIndex, range }, (_, dbRemoteDoc, control) => {
- const potentialKey = DocumentKey.fromSegments([
- ...dbRemoteDoc.prefixPath,
- dbRemoteDoc.collectionGroup,
- dbRemoteDoc.documentId
- ]);
- // Go through keys not found in cache.
- while (nextKey && dbKeyComparator(nextKey, potentialKey) < 0) {
- callback(nextKey, null);
- nextKey = keyIter.getNext();
- }
- if (nextKey && nextKey.isEqual(potentialKey)) {
- // Key found in cache.
- callback(nextKey, dbRemoteDoc);
- nextKey = keyIter.hasNext() ? keyIter.getNext() : null;
- }
- // Skip to the next key (if there is one).
- if (nextKey) {
- control.skip(dbKey(nextKey));
- }
- else {
- control.done();
- }
- })
- .next(() => {
- // The rest of the keys are not in the cache. One case where `iterate`
- // above won't go through them is when the cache is empty.
- while (nextKey) {
- callback(nextKey, null);
- nextKey = keyIter.hasNext() ? keyIter.getNext() : null;
- }
- });
- }
- getDocumentsMatchingQuery(transaction, query, offset, mutatedDocs) {
- const collection = query.path;
- const startKey = [
- collection.popLast().toArray(),
- collection.lastSegment(),
- toDbTimestampKey(offset.readTime),
- offset.documentKey.path.isEmpty()
- ? ''
- : offset.documentKey.path.lastSegment()
- ];
- const endKey = [
- collection.popLast().toArray(),
- collection.lastSegment(),
- [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER],
- ''
- ];
- return remoteDocumentsStore(transaction)
- .loadAll(IDBKeyRange.bound(startKey, endKey, true))
- .next(dbRemoteDocs => {
- let results = mutableDocumentMap();
- for (const dbRemoteDoc of dbRemoteDocs) {
- const document = this.maybeDecodeDocument(DocumentKey.fromSegments(dbRemoteDoc.prefixPath.concat(dbRemoteDoc.collectionGroup, dbRemoteDoc.documentId)), dbRemoteDoc);
- if (document.isFoundDocument() &&
- (queryMatches(query, document) || mutatedDocs.has(document.key))) {
- // Either the document matches the given query, or it is mutated.
- results = results.insert(document.key, document);
- }
- }
- return results;
- });
- }
- getAllFromCollectionGroup(transaction, collectionGroup, offset, limit) {
- let results = mutableDocumentMap();
- const startKey = dbCollectionGroupKey(collectionGroup, offset);
- const endKey = dbCollectionGroupKey(collectionGroup, IndexOffset.max());
- return remoteDocumentsStore(transaction)
- .iterate({
- index: DbRemoteDocumentCollectionGroupIndex,
- range: IDBKeyRange.bound(startKey, endKey, true)
- }, (_, dbRemoteDoc, control) => {
- const document = this.maybeDecodeDocument(DocumentKey.fromSegments(dbRemoteDoc.prefixPath.concat(dbRemoteDoc.collectionGroup, dbRemoteDoc.documentId)), dbRemoteDoc);
- results = results.insert(document.key, document);
- if (results.size === limit) {
- control.done();
- }
- })
- .next(() => results);
- }
- newChangeBuffer(options) {
- return new IndexedDbRemoteDocumentChangeBuffer(this, !!options && options.trackRemovals);
- }
- getSize(txn) {
- return this.getMetadata(txn).next(metadata => metadata.byteSize);
- }
- getMetadata(txn) {
- return documentGlobalStore(txn)
- .get(DbRemoteDocumentGlobalKey)
- .next(metadata => {
- hardAssert(!!metadata);
- return metadata;
- });
- }
- setMetadata(txn, metadata) {
- return documentGlobalStore(txn).put(DbRemoteDocumentGlobalKey, metadata);
- }
- /**
- * Decodes `dbRemoteDoc` and returns the document (or an invalid document if
- * the document corresponds to the format used for sentinel deletes).
- */
- maybeDecodeDocument(documentKey, dbRemoteDoc) {
- if (dbRemoteDoc) {
- const doc = fromDbRemoteDocument(this.serializer, dbRemoteDoc);
- // Whether the document is a sentinel removal and should only be used in the
- // `getNewDocumentChanges()`
- const isSentinelRemoval = doc.isNoDocument() && doc.version.isEqual(SnapshotVersion.min());
- if (!isSentinelRemoval) {
- return doc;
- }
- }
- return MutableDocument.newInvalidDocument(documentKey);
- }
- }
- /** Creates a new IndexedDbRemoteDocumentCache. */
- function newIndexedDbRemoteDocumentCache(serializer) {
- return new IndexedDbRemoteDocumentCacheImpl(serializer);
- }
- /**
- * Handles the details of adding and updating documents in the IndexedDbRemoteDocumentCache.
- *
- * Unlike the MemoryRemoteDocumentChangeBuffer, the IndexedDb implementation computes the size
- * delta for all submitted changes. This avoids having to re-read all documents from IndexedDb
- * when we apply the changes.
- */
- class IndexedDbRemoteDocumentChangeBuffer extends RemoteDocumentChangeBuffer {
- /**
- * @param documentCache - The IndexedDbRemoteDocumentCache to apply the changes to.
- * @param trackRemovals - Whether to create sentinel deletes that can be tracked by
- * `getNewDocumentChanges()`.
- */
- constructor(documentCache, trackRemovals) {
- super();
- this.documentCache = documentCache;
- this.trackRemovals = trackRemovals;
- // A map of document sizes and read times prior to applying the changes in
- // this buffer.
- this.documentStates = new ObjectMap(key => key.toString(), (l, r) => l.isEqual(r));
- }
- applyChanges(transaction) {
- const promises = [];
- let sizeDelta = 0;
- let collectionParents = new SortedSet((l, r) => primitiveComparator(l.canonicalString(), r.canonicalString()));
- this.changes.forEach((key, documentChange) => {
- const previousDoc = this.documentStates.get(key);
- promises.push(this.documentCache.removeEntry(transaction, key, previousDoc.readTime));
- if (documentChange.isValidDocument()) {
- const doc = toDbRemoteDocument(this.documentCache.serializer, documentChange);
- collectionParents = collectionParents.add(key.path.popLast());
- const size = dbDocumentSize(doc);
- sizeDelta += size - previousDoc.size;
- promises.push(this.documentCache.addEntry(transaction, key, doc));
- }
- else {
- sizeDelta -= previousDoc.size;
- if (this.trackRemovals) {
- // In order to track removals, we store a "sentinel delete" in the
- // RemoteDocumentCache. This entry is represented by a NoDocument
- // with a version of 0 and ignored by `maybeDecodeDocument()` but
- // preserved in `getNewDocumentChanges()`.
- const deletedDoc = toDbRemoteDocument(this.documentCache.serializer, documentChange.convertToNoDocument(SnapshotVersion.min()));
- promises.push(this.documentCache.addEntry(transaction, key, deletedDoc));
- }
- }
- });
- collectionParents.forEach(parent => {
- promises.push(this.documentCache.indexManager.addToCollectionParentIndex(transaction, parent));
- });
- promises.push(this.documentCache.updateMetadata(transaction, sizeDelta));
- return PersistencePromise.waitFor(promises);
- }
- getFromCache(transaction, documentKey) {
- // Record the size of everything we load from the cache so we can compute a delta later.
- return this.documentCache
- .getSizedEntry(transaction, documentKey)
- .next(getResult => {
- this.documentStates.set(documentKey, {
- size: getResult.size,
- readTime: getResult.document.readTime
- });
- return getResult.document;
- });
- }
- getAllFromCache(transaction, documentKeys) {
- // Record the size of everything we load from the cache so we can compute
- // a delta later.
- return this.documentCache
- .getSizedEntries(transaction, documentKeys)
- .next(({ documents, sizeMap }) => {
- // Note: `getAllFromCache` returns two maps instead of a single map from
- // keys to `DocumentSizeEntry`s. This is to allow returning the
- // `MutableDocumentMap` directly, without a conversion.
- sizeMap.forEach((documentKey, size) => {
- this.documentStates.set(documentKey, {
- size,
- readTime: documents.get(documentKey).readTime
- });
- });
- return documents;
- });
- }
- }
- function documentGlobalStore(txn) {
- return getStore(txn, DbRemoteDocumentGlobalStore);
- }
- /**
- * Helper to get a typed SimpleDbStore for the remoteDocuments object store.
- */
- function remoteDocumentsStore(txn) {
- return getStore(txn, DbRemoteDocumentStore);
- }
- /**
- * Returns a key that can be used for document lookups on the
- * `DbRemoteDocumentDocumentKeyIndex` index.
- */
- function dbKey(documentKey) {
- const path = documentKey.path.toArray();
- return [
- /* prefix path */ path.slice(0, path.length - 2),
- /* collection id */ path[path.length - 2],
- /* document id */ path[path.length - 1]
- ];
- }
- /**
- * Returns a key that can be used for document lookups via the primary key of
- * the DbRemoteDocument object store.
- */
- function dbReadTimeKey(documentKey, readTime) {
- const path = documentKey.path.toArray();
- return [
- /* prefix path */ path.slice(0, path.length - 2),
- /* collection id */ path[path.length - 2],
- toDbTimestampKey(readTime),
- /* document id */ path[path.length - 1]
- ];
- }
- /**
- * Returns a key that can be used for document lookups on the
- * `DbRemoteDocumentDocumentCollectionGroupIndex` index.
- */
- function dbCollectionGroupKey(collectionGroup, offset) {
- const path = offset.documentKey.path.toArray();
- return [
- /* collection id */ collectionGroup,
- toDbTimestampKey(offset.readTime),
- /* prefix path */ path.slice(0, path.length - 2),
- /* document id */ path.length > 0 ? path[path.length - 1] : ''
- ];
- }
- /**
- * Comparator that compares document keys according to the primary key sorting
- * used by the `DbRemoteDocumentDocument` store (by prefix path, collection id
- * and then document ID).
- *
- * Visible for testing.
- */
- function dbKeyComparator(l, r) {
- const left = l.path.toArray();
- const right = r.path.toArray();
- // The ordering is based on https://chromium.googlesource.com/chromium/blink/+/fe5c21fef94dae71c1c3344775b8d8a7f7e6d9ec/Source/modules/indexeddb/IDBKey.cpp#74
- let cmp = 0;
- for (let i = 0; i < left.length - 2 && i < right.length - 2; ++i) {
- cmp = primitiveComparator(left[i], right[i]);
- if (cmp) {
- return cmp;
- }
- }
- cmp = primitiveComparator(left.length, right.length);
- if (cmp) {
- return cmp;
- }
- cmp = primitiveComparator(left[left.length - 2], right[right.length - 2]);
- if (cmp) {
- return cmp;
- }
- return primitiveComparator(left[left.length - 1], right[right.length - 1]);
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Schema Version for the Web client:
- * 1. Initial version including Mutation Queue, Query Cache, and Remote
- * Document Cache
- * 2. Used to ensure a targetGlobal object exists and add targetCount to it. No
- * longer required because migration 3 unconditionally clears it.
- * 3. Dropped and re-created Query Cache to deal with cache corruption related
- * to limbo resolution. Addresses
- * https://github.com/firebase/firebase-ios-sdk/issues/1548
- * 4. Multi-Tab Support.
- * 5. Removal of held write acks.
- * 6. Create document global for tracking document cache size.
- * 7. Ensure every cached document has a sentinel row with a sequence number.
- * 8. Add collection-parent index for Collection Group queries.
- * 9. Change RemoteDocumentChanges store to be keyed by readTime rather than
- * an auto-incrementing ID. This is required for Index-Free queries.
- * 10. Rewrite the canonical IDs to the explicit Protobuf-based format.
- * 11. Add bundles and named_queries for bundle support.
- * 12. Add document overlays.
- * 13. Rewrite the keys of the remote document cache to allow for efficient
- * document lookup via `getAll()`.
- * 14. Add overlays.
- * 15. Add indexing support.
- */
- const SCHEMA_VERSION = 15;
- /**
- * @license
- * Copyright 2022 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Represents a local view (overlay) of a document, and the fields that are
- * locally mutated.
- */
- class OverlayedDocument {
- constructor(overlayedDocument,
- /**
- * The fields that are locally mutated by patch mutations.
- *
- * If the overlayed document is from set or delete mutations, this is `null`.
- * If there is no overlay (mutation) for the document, this is an empty `FieldMask`.
- */
- mutatedFields) {
- this.overlayedDocument = overlayedDocument;
- this.mutatedFields = mutatedFields;
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * A readonly view of the local state of all documents we're tracking (i.e. we
- * have a cached version in remoteDocumentCache or local mutations for the
- * document). The view is computed by applying the mutations in the
- * MutationQueue to the RemoteDocumentCache.
- */
- class LocalDocumentsView {
- constructor(remoteDocumentCache, mutationQueue, documentOverlayCache, indexManager) {
- this.remoteDocumentCache = remoteDocumentCache;
- this.mutationQueue = mutationQueue;
- this.documentOverlayCache = documentOverlayCache;
- this.indexManager = indexManager;
- }
- /**
- * Get the local view of the document identified by `key`.
- *
- * @returns Local view of the document or null if we don't have any cached
- * state for it.
- */
- getDocument(transaction, key) {
- let overlay = null;
- return this.documentOverlayCache
- .getOverlay(transaction, key)
- .next(value => {
- overlay = value;
- return this.remoteDocumentCache.getEntry(transaction, key);
- })
- .next(document => {
- if (overlay !== null) {
- mutationApplyToLocalView(overlay.mutation, document, FieldMask.empty(), Timestamp.now());
- }
- return document;
- });
- }
- /**
- * Gets the local view of the documents identified by `keys`.
- *
- * If we don't have cached state for a document in `keys`, a NoDocument will
- * be stored for that key in the resulting set.
- */
- getDocuments(transaction, keys) {
- return this.remoteDocumentCache
- .getEntries(transaction, keys)
- .next(docs => this.getLocalViewOfDocuments(transaction, docs, documentKeySet()).next(() => docs));
- }
- /**
- * Similar to `getDocuments`, but creates the local view from the given
- * `baseDocs` without retrieving documents from the local store.
- *
- * @param transaction - The transaction this operation is scoped to.
- * @param docs - The documents to apply local mutations to get the local views.
- * @param existenceStateChanged - The set of document keys whose existence state
- * is changed. This is useful to determine if some documents overlay needs
- * to be recalculated.
- */
- getLocalViewOfDocuments(transaction, docs, existenceStateChanged = documentKeySet()) {
- const overlays = newOverlayMap();
- return this.populateOverlays(transaction, overlays, docs).next(() => {
- return this.computeViews(transaction, docs, overlays, existenceStateChanged).next(computeViewsResult => {
- let result = documentMap();
- computeViewsResult.forEach((documentKey, overlayedDocument) => {
- result = result.insert(documentKey, overlayedDocument.overlayedDocument);
- });
- return result;
- });
- });
- }
- /**
- * Gets the overlayed documents for the given document map, which will include
- * the local view of those documents and a `FieldMask` indicating which fields
- * are mutated locally, `null` if overlay is a Set or Delete mutation.
- */
- getOverlayedDocuments(transaction, docs) {
- const overlays = newOverlayMap();
- return this.populateOverlays(transaction, overlays, docs).next(() => this.computeViews(transaction, docs, overlays, documentKeySet()));
- }
- /**
- * Fetches the overlays for {@code docs} and adds them to provided overlay map
- * if the map does not already contain an entry for the given document key.
- */
- populateOverlays(transaction, overlays, docs) {
- const missingOverlays = [];
- docs.forEach(key => {
- if (!overlays.has(key)) {
- missingOverlays.push(key);
- }
- });
- return this.documentOverlayCache
- .getOverlays(transaction, missingOverlays)
- .next(result => {
- result.forEach((key, val) => {
- overlays.set(key, val);
- });
- });
- }
- /**
- * Computes the local view for the given documents.
- *
- * @param docs - The documents to compute views for. It also has the base
- * version of the documents.
- * @param overlays - The overlays that need to be applied to the given base
- * version of the documents.
- * @param existenceStateChanged - A set of documents whose existence states
- * might have changed. This is used to determine if we need to re-calculate
- * overlays from mutation queues.
- * @return A map represents the local documents view.
- */
- computeViews(transaction, docs, overlays, existenceStateChanged) {
- let recalculateDocuments = mutableDocumentMap();
- const mutatedFields = newDocumentKeyMap();
- const results = newOverlayedDocumentMap();
- docs.forEach((_, doc) => {
- const overlay = overlays.get(doc.key);
- // Recalculate an overlay if the document's existence state changed due to
- // a remote event *and* the overlay is a PatchMutation. This is because
- // document existence state can change if some patch mutation's
- // preconditions are met.
- // NOTE: we recalculate when `overlay` is undefined as well, because there
- // might be a patch mutation whose precondition does not match before the
- // change (hence overlay is undefined), but would now match.
- if (existenceStateChanged.has(doc.key) &&
- (overlay === undefined || overlay.mutation instanceof PatchMutation)) {
- recalculateDocuments = recalculateDocuments.insert(doc.key, doc);
- }
- else if (overlay !== undefined) {
- mutatedFields.set(doc.key, overlay.mutation.getFieldMask());
- mutationApplyToLocalView(overlay.mutation, doc, overlay.mutation.getFieldMask(), Timestamp.now());
- }
- else {
- // no overlay exists
- // Using EMPTY to indicate there is no overlay for the document.
- mutatedFields.set(doc.key, FieldMask.empty());
- }
- });
- return this.recalculateAndSaveOverlays(transaction, recalculateDocuments).next(recalculatedFields => {
- recalculatedFields.forEach((documentKey, mask) => mutatedFields.set(documentKey, mask));
- docs.forEach((documentKey, document) => {
- var _a;
- return results.set(documentKey, new OverlayedDocument(document, (_a = mutatedFields.get(documentKey)) !== null && _a !== void 0 ? _a : null));
- });
- return results;
- });
- }
- recalculateAndSaveOverlays(transaction, docs) {
- const masks = newDocumentKeyMap();
- // A reverse lookup map from batch id to the documents within that batch.
- let documentsByBatchId = new SortedMap((key1, key2) => key1 - key2);
- let processed = documentKeySet();
- return this.mutationQueue
- .getAllMutationBatchesAffectingDocumentKeys(transaction, docs)
- .next(batches => {
- for (const batch of batches) {
- batch.keys().forEach(key => {
- const baseDoc = docs.get(key);
- if (baseDoc === null) {
- return;
- }
- let mask = masks.get(key) || FieldMask.empty();
- mask = batch.applyToLocalView(baseDoc, mask);
- masks.set(key, mask);
- const newSet = (documentsByBatchId.get(batch.batchId) || documentKeySet()).add(key);
- documentsByBatchId = documentsByBatchId.insert(batch.batchId, newSet);
- });
- }
- })
- .next(() => {
- const promises = [];
- // Iterate in descending order of batch IDs, and skip documents that are
- // already saved.
- const iter = documentsByBatchId.getReverseIterator();
- while (iter.hasNext()) {
- const entry = iter.getNext();
- const batchId = entry.key;
- const keys = entry.value;
- const overlays = newMutationMap();
- keys.forEach(key => {
- if (!processed.has(key)) {
- const overlayMutation = calculateOverlayMutation(docs.get(key), masks.get(key));
- if (overlayMutation !== null) {
- overlays.set(key, overlayMutation);
- }
- processed = processed.add(key);
- }
- });
- promises.push(this.documentOverlayCache.saveOverlays(transaction, batchId, overlays));
- }
- return PersistencePromise.waitFor(promises);
- })
- .next(() => masks);
- }
- /**
- * Recalculates overlays by reading the documents from remote document cache
- * first, and saves them after they are calculated.
- */
- recalculateAndSaveOverlaysForDocumentKeys(transaction, documentKeys) {
- return this.remoteDocumentCache
- .getEntries(transaction, documentKeys)
- .next(docs => this.recalculateAndSaveOverlays(transaction, docs));
- }
- /**
- * Performs a query against the local view of all documents.
- *
- * @param transaction - The persistence transaction.
- * @param query - The query to match documents against.
- * @param offset - Read time and key to start scanning by (exclusive).
- */
- getDocumentsMatchingQuery(transaction, query, offset) {
- if (isDocumentQuery$1(query)) {
- return this.getDocumentsMatchingDocumentQuery(transaction, query.path);
- }
- else if (isCollectionGroupQuery(query)) {
- return this.getDocumentsMatchingCollectionGroupQuery(transaction, query, offset);
- }
- else {
- return this.getDocumentsMatchingCollectionQuery(transaction, query, offset);
- }
- }
- /**
- * Given a collection group, returns the next documents that follow the provided offset, along
- * with an updated batch ID.
- *
- * <p>The documents returned by this method are ordered by remote version from the provided
- * offset. If there are no more remote documents after the provided offset, documents with
- * mutations in order of batch id from the offset are returned. Since all documents in a batch are
- * returned together, the total number of documents returned can exceed {@code count}.
- *
- * @param transaction
- * @param collectionGroup The collection group for the documents.
- * @param offset The offset to index into.
- * @param count The number of documents to return
- * @return A LocalWriteResult with the documents that follow the provided offset and the last processed batch id.
- */
- getNextDocuments(transaction, collectionGroup, offset, count) {
- return this.remoteDocumentCache
- .getAllFromCollectionGroup(transaction, collectionGroup, offset, count)
- .next((originalDocs) => {
- const overlaysPromise = count - originalDocs.size > 0
- ? this.documentOverlayCache.getOverlaysForCollectionGroup(transaction, collectionGroup, offset.largestBatchId, count - originalDocs.size)
- : PersistencePromise.resolve(newOverlayMap());
- // The callsite will use the largest batch ID together with the latest read time to create
- // a new index offset. Since we only process batch IDs if all remote documents have been read,
- // no overlay will increase the overall read time. This is why we only need to special case
- // the batch id.
- let largestBatchId = INITIAL_LARGEST_BATCH_ID;
- let modifiedDocs = originalDocs;
- return overlaysPromise.next(overlays => {
- return PersistencePromise.forEach(overlays, (key, overlay) => {
- if (largestBatchId < overlay.largestBatchId) {
- largestBatchId = overlay.largestBatchId;
- }
- if (originalDocs.get(key)) {
- return PersistencePromise.resolve();
- }
- return this.remoteDocumentCache
- .getEntry(transaction, key)
- .next(doc => {
- modifiedDocs = modifiedDocs.insert(key, doc);
- });
- })
- .next(() => this.populateOverlays(transaction, overlays, originalDocs))
- .next(() => this.computeViews(transaction, modifiedDocs, overlays, documentKeySet()))
- .next(localDocs => ({
- batchId: largestBatchId,
- changes: convertOverlayedDocumentMapToDocumentMap(localDocs)
- }));
- });
- });
- }
- getDocumentsMatchingDocumentQuery(transaction, docPath) {
- // Just do a simple document lookup.
- return this.getDocument(transaction, new DocumentKey(docPath)).next(document => {
- let result = documentMap();
- if (document.isFoundDocument()) {
- result = result.insert(document.key, document);
- }
- return result;
- });
- }
- getDocumentsMatchingCollectionGroupQuery(transaction, query, offset) {
- const collectionId = query.collectionGroup;
- let results = documentMap();
- return this.indexManager
- .getCollectionParents(transaction, collectionId)
- .next(parents => {
- // Perform a collection query against each parent that contains the
- // collectionId and aggregate the results.
- return PersistencePromise.forEach(parents, (parent) => {
- const collectionQuery = asCollectionQueryAtPath(query, parent.child(collectionId));
- return this.getDocumentsMatchingCollectionQuery(transaction, collectionQuery, offset).next(r => {
- r.forEach((key, doc) => {
- results = results.insert(key, doc);
- });
- });
- }).next(() => results);
- });
- }
- getDocumentsMatchingCollectionQuery(transaction, query, offset) {
- // Query the remote documents and overlay mutations.
- let overlays;
- return this.documentOverlayCache
- .getOverlaysForCollection(transaction, query.path, offset.largestBatchId)
- .next(result => {
- overlays = result;
- return this.remoteDocumentCache.getDocumentsMatchingQuery(transaction, query, offset, overlays);
- })
- .next(remoteDocuments => {
- // As documents might match the query because of their overlay we need to
- // include documents for all overlays in the initial document set.
- overlays.forEach((_, overlay) => {
- const key = overlay.getKey();
- if (remoteDocuments.get(key) === null) {
- remoteDocuments = remoteDocuments.insert(key, MutableDocument.newInvalidDocument(key));
- }
- });
- // Apply the overlays and match against the query.
- let results = documentMap();
- remoteDocuments.forEach((key, document) => {
- const overlay = overlays.get(key);
- if (overlay !== undefined) {
- mutationApplyToLocalView(overlay.mutation, document, FieldMask.empty(), Timestamp.now());
- }
- // Finally, insert the documents that still match the query
- if (queryMatches(query, document)) {
- results = results.insert(key, document);
- }
- });
- return results;
- });
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- class MemoryBundleCache {
- constructor(serializer) {
- this.serializer = serializer;
- this.bundles = new Map();
- this.namedQueries = new Map();
- }
- getBundleMetadata(transaction, bundleId) {
- return PersistencePromise.resolve(this.bundles.get(bundleId));
- }
- saveBundleMetadata(transaction, bundleMetadata) {
- this.bundles.set(bundleMetadata.id, fromBundleMetadata(bundleMetadata));
- return PersistencePromise.resolve();
- }
- getNamedQuery(transaction, queryName) {
- return PersistencePromise.resolve(this.namedQueries.get(queryName));
- }
- saveNamedQuery(transaction, query) {
- this.namedQueries.set(query.name, fromProtoNamedQuery(query));
- return PersistencePromise.resolve();
- }
- }
- /**
- * @license
- * Copyright 2022 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * An in-memory implementation of DocumentOverlayCache.
- */
- class MemoryDocumentOverlayCache {
- constructor() {
- // A map sorted by DocumentKey, whose value is a pair of the largest batch id
- // for the overlay and the overlay itself.
- this.overlays = new SortedMap(DocumentKey.comparator);
- this.overlayByBatchId = new Map();
- }
- getOverlay(transaction, key) {
- return PersistencePromise.resolve(this.overlays.get(key));
- }
- getOverlays(transaction, keys) {
- const result = newOverlayMap();
- return PersistencePromise.forEach(keys, (key) => {
- return this.getOverlay(transaction, key).next(overlay => {
- if (overlay !== null) {
- result.set(key, overlay);
- }
- });
- }).next(() => result);
- }
- saveOverlays(transaction, largestBatchId, overlays) {
- overlays.forEach((_, mutation) => {
- this.saveOverlay(transaction, largestBatchId, mutation);
- });
- return PersistencePromise.resolve();
- }
- removeOverlaysForBatchId(transaction, documentKeys, batchId) {
- const keys = this.overlayByBatchId.get(batchId);
- if (keys !== undefined) {
- keys.forEach(key => (this.overlays = this.overlays.remove(key)));
- this.overlayByBatchId.delete(batchId);
- }
- return PersistencePromise.resolve();
- }
- getOverlaysForCollection(transaction, collection, sinceBatchId) {
- const result = newOverlayMap();
- const immediateChildrenPathLength = collection.length + 1;
- const prefix = new DocumentKey(collection.child(''));
- const iter = this.overlays.getIteratorFrom(prefix);
- while (iter.hasNext()) {
- const entry = iter.getNext();
- const overlay = entry.value;
- const key = overlay.getKey();
- if (!collection.isPrefixOf(key.path)) {
- break;
- }
- // Documents from sub-collections
- if (key.path.length !== immediateChildrenPathLength) {
- continue;
- }
- if (overlay.largestBatchId > sinceBatchId) {
- result.set(overlay.getKey(), overlay);
- }
- }
- return PersistencePromise.resolve(result);
- }
- getOverlaysForCollectionGroup(transaction, collectionGroup, sinceBatchId, count) {
- let batchIdToOverlays = new SortedMap((key1, key2) => key1 - key2);
- const iter = this.overlays.getIterator();
- while (iter.hasNext()) {
- const entry = iter.getNext();
- const overlay = entry.value;
- const key = overlay.getKey();
- if (key.getCollectionGroup() !== collectionGroup) {
- continue;
- }
- if (overlay.largestBatchId > sinceBatchId) {
- let overlaysForBatchId = batchIdToOverlays.get(overlay.largestBatchId);
- if (overlaysForBatchId === null) {
- overlaysForBatchId = newOverlayMap();
- batchIdToOverlays = batchIdToOverlays.insert(overlay.largestBatchId, overlaysForBatchId);
- }
- overlaysForBatchId.set(overlay.getKey(), overlay);
- }
- }
- const result = newOverlayMap();
- const batchIter = batchIdToOverlays.getIterator();
- while (batchIter.hasNext()) {
- const entry = batchIter.getNext();
- const overlays = entry.value;
- overlays.forEach((key, overlay) => result.set(key, overlay));
- if (result.size() >= count) {
- break;
- }
- }
- return PersistencePromise.resolve(result);
- }
- saveOverlay(transaction, largestBatchId, mutation) {
- // Remove the association of the overlay to its batch id.
- const existing = this.overlays.get(mutation.key);
- if (existing !== null) {
- const newSet = this.overlayByBatchId
- .get(existing.largestBatchId)
- .delete(mutation.key);
- this.overlayByBatchId.set(existing.largestBatchId, newSet);
- }
- this.overlays = this.overlays.insert(mutation.key, new Overlay(largestBatchId, mutation));
- // Create the association of this overlay to the given largestBatchId.
- let batch = this.overlayByBatchId.get(largestBatchId);
- if (batch === undefined) {
- batch = documentKeySet();
- this.overlayByBatchId.set(largestBatchId, batch);
- }
- this.overlayByBatchId.set(largestBatchId, batch.add(mutation.key));
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * A collection of references to a document from some kind of numbered entity
- * (either a target ID or batch ID). As references are added to or removed from
- * the set corresponding events are emitted to a registered garbage collector.
- *
- * Each reference is represented by a DocumentReference object. Each of them
- * contains enough information to uniquely identify the reference. They are all
- * stored primarily in a set sorted by key. A document is considered garbage if
- * there's no references in that set (this can be efficiently checked thanks to
- * sorting by key).
- *
- * ReferenceSet also keeps a secondary set that contains references sorted by
- * IDs. This one is used to efficiently implement removal of all references by
- * some target ID.
- */
- class ReferenceSet {
- constructor() {
- // A set of outstanding references to a document sorted by key.
- this.refsByKey = new SortedSet(DocReference.compareByKey);
- // A set of outstanding references to a document sorted by target id.
- this.refsByTarget = new SortedSet(DocReference.compareByTargetId);
- }
- /** Returns true if the reference set contains no references. */
- isEmpty() {
- return this.refsByKey.isEmpty();
- }
- /** Adds a reference to the given document key for the given ID. */
- addReference(key, id) {
- const ref = new DocReference(key, id);
- this.refsByKey = this.refsByKey.add(ref);
- this.refsByTarget = this.refsByTarget.add(ref);
- }
- /** Add references to the given document keys for the given ID. */
- addReferences(keys, id) {
- keys.forEach(key => this.addReference(key, id));
- }
- /**
- * Removes a reference to the given document key for the given
- * ID.
- */
- removeReference(key, id) {
- this.removeRef(new DocReference(key, id));
- }
- removeReferences(keys, id) {
- keys.forEach(key => this.removeReference(key, id));
- }
- /**
- * Clears all references with a given ID. Calls removeRef() for each key
- * removed.
- */
- removeReferencesForId(id) {
- const emptyKey = new DocumentKey(new ResourcePath([]));
- const startRef = new DocReference(emptyKey, id);
- const endRef = new DocReference(emptyKey, id + 1);
- const keys = [];
- this.refsByTarget.forEachInRange([startRef, endRef], ref => {
- this.removeRef(ref);
- keys.push(ref.key);
- });
- return keys;
- }
- removeAllReferences() {
- this.refsByKey.forEach(ref => this.removeRef(ref));
- }
- removeRef(ref) {
- this.refsByKey = this.refsByKey.delete(ref);
- this.refsByTarget = this.refsByTarget.delete(ref);
- }
- referencesForId(id) {
- const emptyKey = new DocumentKey(new ResourcePath([]));
- const startRef = new DocReference(emptyKey, id);
- const endRef = new DocReference(emptyKey, id + 1);
- let keys = documentKeySet();
- this.refsByTarget.forEachInRange([startRef, endRef], ref => {
- keys = keys.add(ref.key);
- });
- return keys;
- }
- containsKey(key) {
- const ref = new DocReference(key, 0);
- const firstRef = this.refsByKey.firstAfterOrEqual(ref);
- return firstRef !== null && key.isEqual(firstRef.key);
- }
- }
- class DocReference {
- constructor(key, targetOrBatchId) {
- this.key = key;
- this.targetOrBatchId = targetOrBatchId;
- }
- /** Compare by key then by ID */
- static compareByKey(left, right) {
- return (DocumentKey.comparator(left.key, right.key) ||
- primitiveComparator(left.targetOrBatchId, right.targetOrBatchId));
- }
- /** Compare by ID then by key */
- static compareByTargetId(left, right) {
- return (primitiveComparator(left.targetOrBatchId, right.targetOrBatchId) ||
- DocumentKey.comparator(left.key, right.key));
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- class MemoryMutationQueue {
- constructor(indexManager, referenceDelegate) {
- this.indexManager = indexManager;
- this.referenceDelegate = referenceDelegate;
- /**
- * The set of all mutations that have been sent but not yet been applied to
- * the backend.
- */
- this.mutationQueue = [];
- /** Next value to use when assigning sequential IDs to each mutation batch. */
- this.nextBatchId = 1;
- /** An ordered mapping between documents and the mutations batch IDs. */
- this.batchesByDocumentKey = new SortedSet(DocReference.compareByKey);
- }
- checkEmpty(transaction) {
- return PersistencePromise.resolve(this.mutationQueue.length === 0);
- }
- addMutationBatch(transaction, localWriteTime, baseMutations, mutations) {
- const batchId = this.nextBatchId;
- this.nextBatchId++;
- if (this.mutationQueue.length > 0) {
- this.mutationQueue[this.mutationQueue.length - 1];
- }
- const batch = new MutationBatch(batchId, localWriteTime, baseMutations, mutations);
- this.mutationQueue.push(batch);
- // Track references by document key and index collection parents.
- for (const mutation of mutations) {
- this.batchesByDocumentKey = this.batchesByDocumentKey.add(new DocReference(mutation.key, batchId));
- this.indexManager.addToCollectionParentIndex(transaction, mutation.key.path.popLast());
- }
- return PersistencePromise.resolve(batch);
- }
- lookupMutationBatch(transaction, batchId) {
- return PersistencePromise.resolve(this.findMutationBatch(batchId));
- }
- getNextMutationBatchAfterBatchId(transaction, batchId) {
- const nextBatchId = batchId + 1;
- // The requested batchId may still be out of range so normalize it to the
- // start of the queue.
- const rawIndex = this.indexOfBatchId(nextBatchId);
- const index = rawIndex < 0 ? 0 : rawIndex;
- return PersistencePromise.resolve(this.mutationQueue.length > index ? this.mutationQueue[index] : null);
- }
- getHighestUnacknowledgedBatchId() {
- return PersistencePromise.resolve(this.mutationQueue.length === 0 ? BATCHID_UNKNOWN : this.nextBatchId - 1);
- }
- getAllMutationBatches(transaction) {
- return PersistencePromise.resolve(this.mutationQueue.slice());
- }
- getAllMutationBatchesAffectingDocumentKey(transaction, documentKey) {
- const start = new DocReference(documentKey, 0);
- const end = new DocReference(documentKey, Number.POSITIVE_INFINITY);
- const result = [];
- this.batchesByDocumentKey.forEachInRange([start, end], ref => {
- const batch = this.findMutationBatch(ref.targetOrBatchId);
- result.push(batch);
- });
- return PersistencePromise.resolve(result);
- }
- getAllMutationBatchesAffectingDocumentKeys(transaction, documentKeys) {
- let uniqueBatchIDs = new SortedSet(primitiveComparator);
- documentKeys.forEach(documentKey => {
- const start = new DocReference(documentKey, 0);
- const end = new DocReference(documentKey, Number.POSITIVE_INFINITY);
- this.batchesByDocumentKey.forEachInRange([start, end], ref => {
- uniqueBatchIDs = uniqueBatchIDs.add(ref.targetOrBatchId);
- });
- });
- return PersistencePromise.resolve(this.findMutationBatches(uniqueBatchIDs));
- }
- getAllMutationBatchesAffectingQuery(transaction, query) {
- // Use the query path as a prefix for testing if a document matches the
- // query.
- const prefix = query.path;
- const immediateChildrenPathLength = prefix.length + 1;
- // Construct a document reference for actually scanning the index. Unlike
- // the prefix the document key in this reference must have an even number of
- // segments. The empty segment can be used a suffix of the query path
- // because it precedes all other segments in an ordered traversal.
- let startPath = prefix;
- if (!DocumentKey.isDocumentKey(startPath)) {
- startPath = startPath.child('');
- }
- const start = new DocReference(new DocumentKey(startPath), 0);
- // Find unique batchIDs referenced by all documents potentially matching the
- // query.
- let uniqueBatchIDs = new SortedSet(primitiveComparator);
- this.batchesByDocumentKey.forEachWhile(ref => {
- const rowKeyPath = ref.key.path;
- if (!prefix.isPrefixOf(rowKeyPath)) {
- return false;
- }
- else {
- // Rows with document keys more than one segment longer than the query
- // path can't be matches. For example, a query on 'rooms' can't match
- // the document /rooms/abc/messages/xyx.
- // TODO(mcg): we'll need a different scanner when we implement
- // ancestor queries.
- if (rowKeyPath.length === immediateChildrenPathLength) {
- uniqueBatchIDs = uniqueBatchIDs.add(ref.targetOrBatchId);
- }
- return true;
- }
- }, start);
- return PersistencePromise.resolve(this.findMutationBatches(uniqueBatchIDs));
- }
- findMutationBatches(batchIDs) {
- // Construct an array of matching batches, sorted by batchID to ensure that
- // multiple mutations affecting the same document key are applied in order.
- const result = [];
- batchIDs.forEach(batchId => {
- const batch = this.findMutationBatch(batchId);
- if (batch !== null) {
- result.push(batch);
- }
- });
- return result;
- }
- removeMutationBatch(transaction, batch) {
- // Find the position of the first batch for removal.
- const batchIndex = this.indexOfExistingBatchId(batch.batchId, 'removed');
- hardAssert(batchIndex === 0);
- this.mutationQueue.shift();
- let references = this.batchesByDocumentKey;
- return PersistencePromise.forEach(batch.mutations, (mutation) => {
- const ref = new DocReference(mutation.key, batch.batchId);
- references = references.delete(ref);
- return this.referenceDelegate.markPotentiallyOrphaned(transaction, mutation.key);
- }).next(() => {
- this.batchesByDocumentKey = references;
- });
- }
- removeCachedMutationKeys(batchId) {
- // No-op since the memory mutation queue does not maintain a separate cache.
- }
- containsKey(txn, key) {
- const ref = new DocReference(key, 0);
- const firstRef = this.batchesByDocumentKey.firstAfterOrEqual(ref);
- return PersistencePromise.resolve(key.isEqual(firstRef && firstRef.key));
- }
- performConsistencyCheck(txn) {
- if (this.mutationQueue.length === 0) ;
- return PersistencePromise.resolve();
- }
- /**
- * Finds the index of the given batchId in the mutation queue and asserts that
- * the resulting index is within the bounds of the queue.
- *
- * @param batchId - The batchId to search for
- * @param action - A description of what the caller is doing, phrased in passive
- * form (e.g. "acknowledged" in a routine that acknowledges batches).
- */
- indexOfExistingBatchId(batchId, action) {
- const index = this.indexOfBatchId(batchId);
- return index;
- }
- /**
- * Finds the index of the given batchId in the mutation queue. This operation
- * is O(1).
- *
- * @returns The computed index of the batch with the given batchId, based on
- * the state of the queue. Note this index can be negative if the requested
- * batchId has already been remvoed from the queue or past the end of the
- * queue if the batchId is larger than the last added batch.
- */
- indexOfBatchId(batchId) {
- if (this.mutationQueue.length === 0) {
- // As an index this is past the end of the queue
- return 0;
- }
- // Examine the front of the queue to figure out the difference between the
- // batchId and indexes in the array. Note that since the queue is ordered
- // by batchId, if the first batch has a larger batchId then the requested
- // batchId doesn't exist in the queue.
- const firstBatchId = this.mutationQueue[0].batchId;
- return batchId - firstBatchId;
- }
- /**
- * A version of lookupMutationBatch that doesn't return a promise, this makes
- * other functions that uses this code easier to read and more efficent.
- */
- findMutationBatch(batchId) {
- const index = this.indexOfBatchId(batchId);
- if (index < 0 || index >= this.mutationQueue.length) {
- return null;
- }
- const batch = this.mutationQueue[index];
- return batch;
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- function documentEntryMap() {
- return new SortedMap(DocumentKey.comparator);
- }
- /**
- * The memory-only RemoteDocumentCache for IndexedDb. To construct, invoke
- * `newMemoryRemoteDocumentCache()`.
- */
- class MemoryRemoteDocumentCacheImpl {
- /**
- * @param sizer - Used to assess the size of a document. For eager GC, this is
- * expected to just return 0 to avoid unnecessarily doing the work of
- * calculating the size.
- */
- constructor(sizer) {
- this.sizer = sizer;
- /** Underlying cache of documents and their read times. */
- this.docs = documentEntryMap();
- /** Size of all cached documents. */
- this.size = 0;
- }
- setIndexManager(indexManager) {
- this.indexManager = indexManager;
- }
- /**
- * Adds the supplied entry to the cache and updates the cache size as appropriate.
- *
- * All calls of `addEntry` are required to go through the RemoteDocumentChangeBuffer
- * returned by `newChangeBuffer()`.
- */
- addEntry(transaction, doc) {
- const key = doc.key;
- const entry = this.docs.get(key);
- const previousSize = entry ? entry.size : 0;
- const currentSize = this.sizer(doc);
- this.docs = this.docs.insert(key, {
- document: doc.mutableCopy(),
- size: currentSize
- });
- this.size += currentSize - previousSize;
- return this.indexManager.addToCollectionParentIndex(transaction, key.path.popLast());
- }
- /**
- * Removes the specified entry from the cache and updates the cache size as appropriate.
- *
- * All calls of `removeEntry` are required to go through the RemoteDocumentChangeBuffer
- * returned by `newChangeBuffer()`.
- */
- removeEntry(documentKey) {
- const entry = this.docs.get(documentKey);
- if (entry) {
- this.docs = this.docs.remove(documentKey);
- this.size -= entry.size;
- }
- }
- getEntry(transaction, documentKey) {
- const entry = this.docs.get(documentKey);
- return PersistencePromise.resolve(entry
- ? entry.document.mutableCopy()
- : MutableDocument.newInvalidDocument(documentKey));
- }
- getEntries(transaction, documentKeys) {
- let results = mutableDocumentMap();
- documentKeys.forEach(documentKey => {
- const entry = this.docs.get(documentKey);
- results = results.insert(documentKey, entry
- ? entry.document.mutableCopy()
- : MutableDocument.newInvalidDocument(documentKey));
- });
- return PersistencePromise.resolve(results);
- }
- getDocumentsMatchingQuery(transaction, query, offset, mutatedDocs) {
- let results = mutableDocumentMap();
- // Documents are ordered by key, so we can use a prefix scan to narrow down
- // the documents we need to match the query against.
- const collectionPath = query.path;
- const prefix = new DocumentKey(collectionPath.child(''));
- const iterator = this.docs.getIteratorFrom(prefix);
- while (iterator.hasNext()) {
- const { key, value: { document } } = iterator.getNext();
- if (!collectionPath.isPrefixOf(key.path)) {
- break;
- }
- if (key.path.length > collectionPath.length + 1) {
- // Exclude entries from subcollections.
- continue;
- }
- if (indexOffsetComparator(newIndexOffsetFromDocument(document), offset) <= 0) {
- // The document sorts before the offset.
- continue;
- }
- if (!mutatedDocs.has(document.key) && !queryMatches(query, document)) {
- // The document cannot possibly match the query.
- continue;
- }
- results = results.insert(document.key, document.mutableCopy());
- }
- return PersistencePromise.resolve(results);
- }
- getAllFromCollectionGroup(transaction, collectionGroup, offset, limti) {
- // This method should only be called from the IndexBackfiller if persistence
- // is enabled.
- fail();
- }
- forEachDocumentKey(transaction, f) {
- return PersistencePromise.forEach(this.docs, (key) => f(key));
- }
- newChangeBuffer(options) {
- // `trackRemovals` is ignores since the MemoryRemoteDocumentCache keeps
- // a separate changelog and does not need special handling for removals.
- return new MemoryRemoteDocumentChangeBuffer(this);
- }
- getSize(txn) {
- return PersistencePromise.resolve(this.size);
- }
- }
- /**
- * Creates a new memory-only RemoteDocumentCache.
- *
- * @param sizer - Used to assess the size of a document. For eager GC, this is
- * expected to just return 0 to avoid unnecessarily doing the work of
- * calculating the size.
- */
- function newMemoryRemoteDocumentCache(sizer) {
- return new MemoryRemoteDocumentCacheImpl(sizer);
- }
- /**
- * Handles the details of adding and updating documents in the MemoryRemoteDocumentCache.
- */
- class MemoryRemoteDocumentChangeBuffer extends RemoteDocumentChangeBuffer {
- constructor(documentCache) {
- super();
- this.documentCache = documentCache;
- }
- applyChanges(transaction) {
- const promises = [];
- this.changes.forEach((key, doc) => {
- if (doc.isValidDocument()) {
- promises.push(this.documentCache.addEntry(transaction, doc));
- }
- else {
- this.documentCache.removeEntry(key);
- }
- });
- return PersistencePromise.waitFor(promises);
- }
- getFromCache(transaction, documentKey) {
- return this.documentCache.getEntry(transaction, documentKey);
- }
- getAllFromCache(transaction, documentKeys) {
- return this.documentCache.getEntries(transaction, documentKeys);
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- class MemoryTargetCache {
- constructor(persistence) {
- this.persistence = persistence;
- /**
- * Maps a target to the data about that target
- */
- this.targets = new ObjectMap(t => canonifyTarget(t), targetEquals);
- /** The last received snapshot version. */
- this.lastRemoteSnapshotVersion = SnapshotVersion.min();
- /** The highest numbered target ID encountered. */
- this.highestTargetId = 0;
- /** The highest sequence number encountered. */
- this.highestSequenceNumber = 0;
- /**
- * A ordered bidirectional mapping between documents and the remote target
- * IDs.
- */
- this.references = new ReferenceSet();
- this.targetCount = 0;
- this.targetIdGenerator = TargetIdGenerator.forTargetCache();
- }
- forEachTarget(txn, f) {
- this.targets.forEach((_, targetData) => f(targetData));
- return PersistencePromise.resolve();
- }
- getLastRemoteSnapshotVersion(transaction) {
- return PersistencePromise.resolve(this.lastRemoteSnapshotVersion);
- }
- getHighestSequenceNumber(transaction) {
- return PersistencePromise.resolve(this.highestSequenceNumber);
- }
- allocateTargetId(transaction) {
- this.highestTargetId = this.targetIdGenerator.next();
- return PersistencePromise.resolve(this.highestTargetId);
- }
- setTargetsMetadata(transaction, highestListenSequenceNumber, lastRemoteSnapshotVersion) {
- if (lastRemoteSnapshotVersion) {
- this.lastRemoteSnapshotVersion = lastRemoteSnapshotVersion;
- }
- if (highestListenSequenceNumber > this.highestSequenceNumber) {
- this.highestSequenceNumber = highestListenSequenceNumber;
- }
- return PersistencePromise.resolve();
- }
- saveTargetData(targetData) {
- this.targets.set(targetData.target, targetData);
- const targetId = targetData.targetId;
- if (targetId > this.highestTargetId) {
- this.targetIdGenerator = new TargetIdGenerator(targetId);
- this.highestTargetId = targetId;
- }
- if (targetData.sequenceNumber > this.highestSequenceNumber) {
- this.highestSequenceNumber = targetData.sequenceNumber;
- }
- }
- addTargetData(transaction, targetData) {
- this.saveTargetData(targetData);
- this.targetCount += 1;
- return PersistencePromise.resolve();
- }
- updateTargetData(transaction, targetData) {
- this.saveTargetData(targetData);
- return PersistencePromise.resolve();
- }
- removeTargetData(transaction, targetData) {
- this.targets.delete(targetData.target);
- this.references.removeReferencesForId(targetData.targetId);
- this.targetCount -= 1;
- return PersistencePromise.resolve();
- }
- removeTargets(transaction, upperBound, activeTargetIds) {
- let count = 0;
- const removals = [];
- this.targets.forEach((key, targetData) => {
- if (targetData.sequenceNumber <= upperBound &&
- activeTargetIds.get(targetData.targetId) === null) {
- this.targets.delete(key);
- removals.push(this.removeMatchingKeysForTargetId(transaction, targetData.targetId));
- count++;
- }
- });
- return PersistencePromise.waitFor(removals).next(() => count);
- }
- getTargetCount(transaction) {
- return PersistencePromise.resolve(this.targetCount);
- }
- getTargetData(transaction, target) {
- const targetData = this.targets.get(target) || null;
- return PersistencePromise.resolve(targetData);
- }
- addMatchingKeys(txn, keys, targetId) {
- this.references.addReferences(keys, targetId);
- return PersistencePromise.resolve();
- }
- removeMatchingKeys(txn, keys, targetId) {
- this.references.removeReferences(keys, targetId);
- const referenceDelegate = this.persistence.referenceDelegate;
- const promises = [];
- if (referenceDelegate) {
- keys.forEach(key => {
- promises.push(referenceDelegate.markPotentiallyOrphaned(txn, key));
- });
- }
- return PersistencePromise.waitFor(promises);
- }
- removeMatchingKeysForTargetId(txn, targetId) {
- this.references.removeReferencesForId(targetId);
- return PersistencePromise.resolve();
- }
- getMatchingKeysForTargetId(txn, targetId) {
- const matchingKeys = this.references.referencesForId(targetId);
- return PersistencePromise.resolve(matchingKeys);
- }
- containsKey(txn, key) {
- return PersistencePromise.resolve(this.references.containsKey(key));
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const LOG_TAG$d = 'MemoryPersistence';
- /**
- * A memory-backed instance of Persistence. Data is stored only in RAM and
- * not persisted across sessions.
- */
- class MemoryPersistence {
- /**
- * The constructor accepts a factory for creating a reference delegate. This
- * allows both the delegate and this instance to have strong references to
- * each other without having nullable fields that would then need to be
- * checked or asserted on every access.
- */
- constructor(referenceDelegateFactory, serializer) {
- this.mutationQueues = {};
- this.overlays = {};
- this.listenSequence = new ListenSequence(0);
- this._started = false;
- this._started = true;
- this.referenceDelegate = referenceDelegateFactory(this);
- this.targetCache = new MemoryTargetCache(this);
- const sizer = (doc) => this.referenceDelegate.documentSize(doc);
- this.indexManager = new MemoryIndexManager();
- this.remoteDocumentCache = newMemoryRemoteDocumentCache(sizer);
- this.serializer = new LocalSerializer(serializer);
- this.bundleCache = new MemoryBundleCache(this.serializer);
- }
- start() {
- return Promise.resolve();
- }
- shutdown() {
- // No durable state to ensure is closed on shutdown.
- this._started = false;
- return Promise.resolve();
- }
- get started() {
- return this._started;
- }
- setDatabaseDeletedListener() {
- // No op.
- }
- setNetworkEnabled() {
- // No op.
- }
- getIndexManager(user) {
- // We do not currently support indices for memory persistence, so we can
- // return the same shared instance of the memory index manager.
- return this.indexManager;
- }
- getDocumentOverlayCache(user) {
- let overlay = this.overlays[user.toKey()];
- if (!overlay) {
- overlay = new MemoryDocumentOverlayCache();
- this.overlays[user.toKey()] = overlay;
- }
- return overlay;
- }
- getMutationQueue(user, indexManager) {
- let queue = this.mutationQueues[user.toKey()];
- if (!queue) {
- queue = new MemoryMutationQueue(indexManager, this.referenceDelegate);
- this.mutationQueues[user.toKey()] = queue;
- }
- return queue;
- }
- getTargetCache() {
- return this.targetCache;
- }
- getRemoteDocumentCache() {
- return this.remoteDocumentCache;
- }
- getBundleCache() {
- return this.bundleCache;
- }
- runTransaction(action, mode, transactionOperation) {
- logDebug(LOG_TAG$d, 'Starting transaction:', action);
- const txn = new MemoryTransaction(this.listenSequence.next());
- this.referenceDelegate.onTransactionStarted();
- return transactionOperation(txn)
- .next(result => {
- return this.referenceDelegate
- .onTransactionCommitted(txn)
- .next(() => result);
- })
- .toPromise()
- .then(result => {
- txn.raiseOnCommittedEvent();
- return result;
- });
- }
- mutationQueuesContainKey(transaction, key) {
- return PersistencePromise.or(Object.values(this.mutationQueues).map(queue => () => queue.containsKey(transaction, key)));
- }
- }
- /**
- * Memory persistence is not actually transactional, but future implementations
- * may have transaction-scoped state.
- */
- class MemoryTransaction extends PersistenceTransaction {
- constructor(currentSequenceNumber) {
- super();
- this.currentSequenceNumber = currentSequenceNumber;
- }
- }
- class MemoryEagerDelegate {
- constructor(persistence) {
- this.persistence = persistence;
- /** Tracks all documents that are active in Query views. */
- this.localViewReferences = new ReferenceSet();
- /** The list of documents that are potentially GCed after each transaction. */
- this._orphanedDocuments = null;
- }
- static factory(persistence) {
- return new MemoryEagerDelegate(persistence);
- }
- get orphanedDocuments() {
- if (!this._orphanedDocuments) {
- throw fail();
- }
- else {
- return this._orphanedDocuments;
- }
- }
- addReference(txn, targetId, key) {
- this.localViewReferences.addReference(key, targetId);
- this.orphanedDocuments.delete(key.toString());
- return PersistencePromise.resolve();
- }
- removeReference(txn, targetId, key) {
- this.localViewReferences.removeReference(key, targetId);
- this.orphanedDocuments.add(key.toString());
- return PersistencePromise.resolve();
- }
- markPotentiallyOrphaned(txn, key) {
- this.orphanedDocuments.add(key.toString());
- return PersistencePromise.resolve();
- }
- removeTarget(txn, targetData) {
- const orphaned = this.localViewReferences.removeReferencesForId(targetData.targetId);
- orphaned.forEach(key => this.orphanedDocuments.add(key.toString()));
- const cache = this.persistence.getTargetCache();
- return cache
- .getMatchingKeysForTargetId(txn, targetData.targetId)
- .next(keys => {
- keys.forEach(key => this.orphanedDocuments.add(key.toString()));
- })
- .next(() => cache.removeTargetData(txn, targetData));
- }
- onTransactionStarted() {
- this._orphanedDocuments = new Set();
- }
- onTransactionCommitted(txn) {
- // Remove newly orphaned documents.
- const cache = this.persistence.getRemoteDocumentCache();
- const changeBuffer = cache.newChangeBuffer();
- return PersistencePromise.forEach(this.orphanedDocuments, (path) => {
- const key = DocumentKey.fromPath(path);
- return this.isReferenced(txn, key).next(isReferenced => {
- if (!isReferenced) {
- changeBuffer.removeEntry(key, SnapshotVersion.min());
- }
- });
- }).next(() => {
- this._orphanedDocuments = null;
- return changeBuffer.apply(txn);
- });
- }
- updateLimboDocument(txn, key) {
- return this.isReferenced(txn, key).next(isReferenced => {
- if (isReferenced) {
- this.orphanedDocuments.delete(key.toString());
- }
- else {
- this.orphanedDocuments.add(key.toString());
- }
- });
- }
- documentSize(doc) {
- // For eager GC, we don't care about the document size, there are no size thresholds.
- return 0;
- }
- isReferenced(txn, key) {
- return PersistencePromise.or([
- () => PersistencePromise.resolve(this.localViewReferences.containsKey(key)),
- () => this.persistence.getTargetCache().containsKey(txn, key),
- () => this.persistence.mutationQueuesContainKey(txn, key)
- ]);
- }
- }
- class MemoryLruDelegate {
- constructor(persistence, lruParams) {
- this.persistence = persistence;
- this.orphanedSequenceNumbers = new ObjectMap(k => encodeResourcePath(k.path), (l, r) => l.isEqual(r));
- this.garbageCollector = newLruGarbageCollector(this, lruParams);
- }
- static factory(persistence, lruParams) {
- return new MemoryLruDelegate(persistence, lruParams);
- }
- // No-ops, present so memory persistence doesn't have to care which delegate
- // it has.
- onTransactionStarted() { }
- onTransactionCommitted(txn) {
- return PersistencePromise.resolve();
- }
- forEachTarget(txn, f) {
- return this.persistence.getTargetCache().forEachTarget(txn, f);
- }
- getSequenceNumberCount(txn) {
- const docCountPromise = this.orphanedDocumentCount(txn);
- const targetCountPromise = this.persistence
- .getTargetCache()
- .getTargetCount(txn);
- return targetCountPromise.next(targetCount => docCountPromise.next(docCount => targetCount + docCount));
- }
- orphanedDocumentCount(txn) {
- let orphanedCount = 0;
- return this.forEachOrphanedDocumentSequenceNumber(txn, _ => {
- orphanedCount++;
- }).next(() => orphanedCount);
- }
- forEachOrphanedDocumentSequenceNumber(txn, f) {
- return PersistencePromise.forEach(this.orphanedSequenceNumbers, (key, sequenceNumber) => {
- // Pass in the exact sequence number as the upper bound so we know it won't be pinned by
- // being too recent.
- return this.isPinned(txn, key, sequenceNumber).next(isPinned => {
- if (!isPinned) {
- return f(sequenceNumber);
- }
- else {
- return PersistencePromise.resolve();
- }
- });
- });
- }
- removeTargets(txn, upperBound, activeTargetIds) {
- return this.persistence
- .getTargetCache()
- .removeTargets(txn, upperBound, activeTargetIds);
- }
- removeOrphanedDocuments(txn, upperBound) {
- let count = 0;
- const cache = this.persistence.getRemoteDocumentCache();
- const changeBuffer = cache.newChangeBuffer();
- const p = cache.forEachDocumentKey(txn, key => {
- return this.isPinned(txn, key, upperBound).next(isPinned => {
- if (!isPinned) {
- count++;
- changeBuffer.removeEntry(key, SnapshotVersion.min());
- }
- });
- });
- return p.next(() => changeBuffer.apply(txn)).next(() => count);
- }
- markPotentiallyOrphaned(txn, key) {
- this.orphanedSequenceNumbers.set(key, txn.currentSequenceNumber);
- return PersistencePromise.resolve();
- }
- removeTarget(txn, targetData) {
- const updated = targetData.withSequenceNumber(txn.currentSequenceNumber);
- return this.persistence.getTargetCache().updateTargetData(txn, updated);
- }
- addReference(txn, targetId, key) {
- this.orphanedSequenceNumbers.set(key, txn.currentSequenceNumber);
- return PersistencePromise.resolve();
- }
- removeReference(txn, targetId, key) {
- this.orphanedSequenceNumbers.set(key, txn.currentSequenceNumber);
- return PersistencePromise.resolve();
- }
- updateLimboDocument(txn, key) {
- this.orphanedSequenceNumbers.set(key, txn.currentSequenceNumber);
- return PersistencePromise.resolve();
- }
- documentSize(document) {
- let documentSize = document.key.toString().length;
- if (document.isFoundDocument()) {
- documentSize += estimateByteSize(document.data.value);
- }
- return documentSize;
- }
- isPinned(txn, key, upperBound) {
- return PersistencePromise.or([
- () => this.persistence.mutationQueuesContainKey(txn, key),
- () => this.persistence.getTargetCache().containsKey(txn, key),
- () => {
- const orphanedAt = this.orphanedSequenceNumbers.get(key);
- return PersistencePromise.resolve(orphanedAt !== undefined && orphanedAt > upperBound);
- }
- ]);
- }
- getCacheSize(txn) {
- return this.persistence.getRemoteDocumentCache().getSize(txn);
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /** Performs database creation and schema upgrades. */
- class SchemaConverter {
- constructor(serializer) {
- this.serializer = serializer;
- }
- /**
- * Performs database creation and schema upgrades.
- *
- * Note that in production, this method is only ever used to upgrade the schema
- * to SCHEMA_VERSION. Different values of toVersion are only used for testing
- * and local feature development.
- */
- createOrUpgrade(db, txn, fromVersion, toVersion) {
- const simpleDbTransaction = new SimpleDbTransaction('createOrUpgrade', txn);
- if (fromVersion < 1 && toVersion >= 1) {
- createPrimaryClientStore(db);
- createMutationQueue(db);
- createQueryCache(db);
- createLegacyRemoteDocumentCache(db);
- }
- // Migration 2 to populate the targetGlobal object no longer needed since
- // migration 3 unconditionally clears it.
- let p = PersistencePromise.resolve();
- if (fromVersion < 3 && toVersion >= 3) {
- // Brand new clients don't need to drop and recreate--only clients that
- // potentially have corrupt data.
- if (fromVersion !== 0) {
- dropQueryCache(db);
- createQueryCache(db);
- }
- p = p.next(() => writeEmptyTargetGlobalEntry(simpleDbTransaction));
- }
- if (fromVersion < 4 && toVersion >= 4) {
- if (fromVersion !== 0) {
- // Schema version 3 uses auto-generated keys to generate globally unique
- // mutation batch IDs (this was previously ensured internally by the
- // client). To migrate to the new schema, we have to read all mutations
- // and write them back out. We preserve the existing batch IDs to guarantee
- // consistency with other object stores. Any further mutation batch IDs will
- // be auto-generated.
- p = p.next(() => upgradeMutationBatchSchemaAndMigrateData(db, simpleDbTransaction));
- }
- p = p.next(() => {
- createClientMetadataStore(db);
- });
- }
- if (fromVersion < 5 && toVersion >= 5) {
- p = p.next(() => this.removeAcknowledgedMutations(simpleDbTransaction));
- }
- if (fromVersion < 6 && toVersion >= 6) {
- p = p.next(() => {
- createDocumentGlobalStore(db);
- return this.addDocumentGlobal(simpleDbTransaction);
- });
- }
- if (fromVersion < 7 && toVersion >= 7) {
- p = p.next(() => this.ensureSequenceNumbers(simpleDbTransaction));
- }
- if (fromVersion < 8 && toVersion >= 8) {
- p = p.next(() => this.createCollectionParentIndex(db, simpleDbTransaction));
- }
- if (fromVersion < 9 && toVersion >= 9) {
- p = p.next(() => {
- // Multi-Tab used to manage its own changelog, but this has been moved
- // to the DbRemoteDocument object store itself. Since the previous change
- // log only contained transient data, we can drop its object store.
- dropRemoteDocumentChangesStore(db);
- // Note: Schema version 9 used to create a read time index for the
- // RemoteDocumentCache. This is now done with schema version 13.
- });
- }
- if (fromVersion < 10 && toVersion >= 10) {
- p = p.next(() => this.rewriteCanonicalIds(simpleDbTransaction));
- }
- if (fromVersion < 11 && toVersion >= 11) {
- p = p.next(() => {
- createBundlesStore(db);
- createNamedQueriesStore(db);
- });
- }
- if (fromVersion < 12 && toVersion >= 12) {
- p = p.next(() => {
- createDocumentOverlayStore(db);
- });
- }
- if (fromVersion < 13 && toVersion >= 13) {
- p = p
- .next(() => createRemoteDocumentCache(db))
- .next(() => this.rewriteRemoteDocumentCache(db, simpleDbTransaction))
- .next(() => db.deleteObjectStore(DbRemoteDocumentStore$1));
- }
- if (fromVersion < 14 && toVersion >= 14) {
- p = p.next(() => this.runOverlayMigration(db, simpleDbTransaction));
- }
- if (fromVersion < 15 && toVersion >= 15) {
- p = p.next(() => createFieldIndex(db));
- }
- return p;
- }
- addDocumentGlobal(txn) {
- let byteSize = 0;
- return txn
- .store(DbRemoteDocumentStore$1)
- .iterate((_, doc) => {
- byteSize += dbDocumentSize(doc);
- })
- .next(() => {
- const metadata = { byteSize };
- return txn
- .store(DbRemoteDocumentGlobalStore)
- .put(DbRemoteDocumentGlobalKey, metadata);
- });
- }
- removeAcknowledgedMutations(txn) {
- const queuesStore = txn.store(DbMutationQueueStore);
- const mutationsStore = txn.store(DbMutationBatchStore);
- return queuesStore.loadAll().next(queues => {
- return PersistencePromise.forEach(queues, (queue) => {
- const range = IDBKeyRange.bound([queue.userId, BATCHID_UNKNOWN], [queue.userId, queue.lastAcknowledgedBatchId]);
- return mutationsStore
- .loadAll(DbMutationBatchUserMutationsIndex, range)
- .next(dbBatches => {
- return PersistencePromise.forEach(dbBatches, (dbBatch) => {
- hardAssert(dbBatch.userId === queue.userId);
- const batch = fromDbMutationBatch(this.serializer, dbBatch);
- return removeMutationBatch(txn, queue.userId, batch).next(() => { });
- });
- });
- });
- });
- }
- /**
- * Ensures that every document in the remote document cache has a corresponding sentinel row
- * with a sequence number. Missing rows are given the most recently used sequence number.
- */
- ensureSequenceNumbers(txn) {
- const documentTargetStore = txn.store(DbTargetDocumentStore);
- const documentsStore = txn.store(DbRemoteDocumentStore$1);
- const globalTargetStore = txn.store(DbTargetGlobalStore);
- return globalTargetStore.get(DbTargetGlobalKey).next(metadata => {
- const writeSentinelKey = (path) => {
- return documentTargetStore.put({
- targetId: 0,
- path: encodeResourcePath(path),
- sequenceNumber: metadata.highestListenSequenceNumber
- });
- };
- const promises = [];
- return documentsStore
- .iterate((key, doc) => {
- const path = new ResourcePath(key);
- const docSentinelKey = sentinelKey(path);
- promises.push(documentTargetStore.get(docSentinelKey).next(maybeSentinel => {
- if (!maybeSentinel) {
- return writeSentinelKey(path);
- }
- else {
- return PersistencePromise.resolve();
- }
- }));
- })
- .next(() => PersistencePromise.waitFor(promises));
- });
- }
- createCollectionParentIndex(db, txn) {
- // Create the index.
- db.createObjectStore(DbCollectionParentStore, {
- keyPath: DbCollectionParentKeyPath
- });
- const collectionParentsStore = txn.store(DbCollectionParentStore);
- // Helper to add an index entry iff we haven't already written it.
- const cache = new MemoryCollectionParentIndex();
- const addEntry = (collectionPath) => {
- if (cache.add(collectionPath)) {
- const collectionId = collectionPath.lastSegment();
- const parentPath = collectionPath.popLast();
- return collectionParentsStore.put({
- collectionId,
- parent: encodeResourcePath(parentPath)
- });
- }
- };
- // Index existing remote documents.
- return txn
- .store(DbRemoteDocumentStore$1)
- .iterate({ keysOnly: true }, (pathSegments, _) => {
- const path = new ResourcePath(pathSegments);
- return addEntry(path.popLast());
- })
- .next(() => {
- // Index existing mutations.
- return txn
- .store(DbDocumentMutationStore)
- .iterate({ keysOnly: true }, ([userID, encodedPath, batchId], _) => {
- const path = decodeResourcePath(encodedPath);
- return addEntry(path.popLast());
- });
- });
- }
- rewriteCanonicalIds(txn) {
- const targetStore = txn.store(DbTargetStore);
- return targetStore.iterate((key, originalDbTarget) => {
- const originalTargetData = fromDbTarget(originalDbTarget);
- const updatedDbTarget = toDbTarget(this.serializer, originalTargetData);
- return targetStore.put(updatedDbTarget);
- });
- }
- rewriteRemoteDocumentCache(db, transaction) {
- const legacyRemoteDocumentStore = transaction.store(DbRemoteDocumentStore$1);
- const writes = [];
- return legacyRemoteDocumentStore
- .iterate((_, legacyDocument) => {
- const remoteDocumentStore = transaction.store(DbRemoteDocumentStore);
- const path = extractKey(legacyDocument).path.toArray();
- const dbRemoteDocument = {
- prefixPath: path.slice(0, path.length - 2),
- collectionGroup: path[path.length - 2],
- documentId: path[path.length - 1],
- readTime: legacyDocument.readTime || [0, 0],
- unknownDocument: legacyDocument.unknownDocument,
- noDocument: legacyDocument.noDocument,
- document: legacyDocument.document,
- hasCommittedMutations: !!legacyDocument.hasCommittedMutations
- };
- writes.push(remoteDocumentStore.put(dbRemoteDocument));
- })
- .next(() => PersistencePromise.waitFor(writes));
- }
- runOverlayMigration(db, transaction) {
- const mutationsStore = transaction.store(DbMutationBatchStore);
- const remoteDocumentCache = newIndexedDbRemoteDocumentCache(this.serializer);
- const memoryPersistence = new MemoryPersistence(MemoryEagerDelegate.factory, this.serializer.remoteSerializer);
- return mutationsStore.loadAll().next(dbBatches => {
- const userToDocumentSet = new Map();
- dbBatches.forEach(dbBatch => {
- var _a;
- let documentSet = (_a = userToDocumentSet.get(dbBatch.userId)) !== null && _a !== void 0 ? _a : documentKeySet();
- const batch = fromDbMutationBatch(this.serializer, dbBatch);
- batch.keys().forEach(key => (documentSet = documentSet.add(key)));
- userToDocumentSet.set(dbBatch.userId, documentSet);
- });
- return PersistencePromise.forEach(userToDocumentSet, (allDocumentKeysForUser, userId) => {
- const user = new User(userId);
- const documentOverlayCache = IndexedDbDocumentOverlayCache.forUser(this.serializer, user);
- // NOTE: The index manager and the reference delegate are
- // irrelevant for the purpose of recalculating and saving
- // overlays. We can therefore simply use the memory
- // implementation.
- const indexManager = memoryPersistence.getIndexManager(user);
- const mutationQueue = IndexedDbMutationQueue.forUser(user, this.serializer, indexManager, memoryPersistence.referenceDelegate);
- const localDocumentsView = new LocalDocumentsView(remoteDocumentCache, mutationQueue, documentOverlayCache, indexManager);
- return localDocumentsView
- .recalculateAndSaveOverlaysForDocumentKeys(new IndexedDbTransaction(transaction, ListenSequence.INVALID), allDocumentKeysForUser)
- .next();
- });
- });
- }
- }
- function sentinelKey(path) {
- return [0, encodeResourcePath(path)];
- }
- function createPrimaryClientStore(db) {
- db.createObjectStore(DbPrimaryClientStore);
- }
- function createMutationQueue(db) {
- db.createObjectStore(DbMutationQueueStore, {
- keyPath: DbMutationQueueKeyPath
- });
- const mutationBatchesStore = db.createObjectStore(DbMutationBatchStore, {
- keyPath: DbMutationBatchKeyPath,
- autoIncrement: true
- });
- mutationBatchesStore.createIndex(DbMutationBatchUserMutationsIndex, DbMutationBatchUserMutationsKeyPath, { unique: true });
- db.createObjectStore(DbDocumentMutationStore);
- }
- /**
- * Upgrade function to migrate the 'mutations' store from V1 to V3. Loads
- * and rewrites all data.
- */
- function upgradeMutationBatchSchemaAndMigrateData(db, txn) {
- const v1MutationsStore = txn.store(DbMutationBatchStore);
- return v1MutationsStore.loadAll().next(existingMutations => {
- db.deleteObjectStore(DbMutationBatchStore);
- const mutationsStore = db.createObjectStore(DbMutationBatchStore, {
- keyPath: DbMutationBatchKeyPath,
- autoIncrement: true
- });
- mutationsStore.createIndex(DbMutationBatchUserMutationsIndex, DbMutationBatchUserMutationsKeyPath, { unique: true });
- const v3MutationsStore = txn.store(DbMutationBatchStore);
- const writeAll = existingMutations.map(mutation => v3MutationsStore.put(mutation));
- return PersistencePromise.waitFor(writeAll);
- });
- }
- function createLegacyRemoteDocumentCache(db) {
- db.createObjectStore(DbRemoteDocumentStore$1);
- }
- function createRemoteDocumentCache(db) {
- const remoteDocumentStore = db.createObjectStore(DbRemoteDocumentStore, {
- keyPath: DbRemoteDocumentKeyPath
- });
- remoteDocumentStore.createIndex(DbRemoteDocumentDocumentKeyIndex, DbRemoteDocumentDocumentKeyIndexPath);
- remoteDocumentStore.createIndex(DbRemoteDocumentCollectionGroupIndex, DbRemoteDocumentCollectionGroupIndexPath);
- }
- function createDocumentGlobalStore(db) {
- db.createObjectStore(DbRemoteDocumentGlobalStore);
- }
- function createQueryCache(db) {
- const targetDocumentsStore = db.createObjectStore(DbTargetDocumentStore, {
- keyPath: DbTargetDocumentKeyPath
- });
- targetDocumentsStore.createIndex(DbTargetDocumentDocumentTargetsIndex, DbTargetDocumentDocumentTargetsKeyPath, { unique: true });
- const targetStore = db.createObjectStore(DbTargetStore, {
- keyPath: DbTargetKeyPath
- });
- // NOTE: This is unique only because the TargetId is the suffix.
- targetStore.createIndex(DbTargetQueryTargetsIndexName, DbTargetQueryTargetsKeyPath, { unique: true });
- db.createObjectStore(DbTargetGlobalStore);
- }
- function dropQueryCache(db) {
- db.deleteObjectStore(DbTargetDocumentStore);
- db.deleteObjectStore(DbTargetStore);
- db.deleteObjectStore(DbTargetGlobalStore);
- }
- function dropRemoteDocumentChangesStore(db) {
- if (db.objectStoreNames.contains('remoteDocumentChanges')) {
- db.deleteObjectStore('remoteDocumentChanges');
- }
- }
- /**
- * Creates the target global singleton row.
- *
- * @param txn - The version upgrade transaction for indexeddb
- */
- function writeEmptyTargetGlobalEntry(txn) {
- const globalStore = txn.store(DbTargetGlobalStore);
- const metadata = {
- highestTargetId: 0,
- highestListenSequenceNumber: 0,
- lastRemoteSnapshotVersion: SnapshotVersion.min().toTimestamp(),
- targetCount: 0
- };
- return globalStore.put(DbTargetGlobalKey, metadata);
- }
- function createClientMetadataStore(db) {
- db.createObjectStore(DbClientMetadataStore, {
- keyPath: DbClientMetadataKeyPath
- });
- }
- function createBundlesStore(db) {
- db.createObjectStore(DbBundleStore, {
- keyPath: DbBundleKeyPath
- });
- }
- function createNamedQueriesStore(db) {
- db.createObjectStore(DbNamedQueryStore, {
- keyPath: DbNamedQueryKeyPath
- });
- }
- function createFieldIndex(db) {
- const indexConfigurationStore = db.createObjectStore(DbIndexConfigurationStore, {
- keyPath: DbIndexConfigurationKeyPath,
- autoIncrement: true
- });
- indexConfigurationStore.createIndex(DbIndexConfigurationCollectionGroupIndex, DbIndexConfigurationCollectionGroupIndexPath, { unique: false });
- const indexStateStore = db.createObjectStore(DbIndexStateStore, {
- keyPath: DbIndexStateKeyPath
- });
- indexStateStore.createIndex(DbIndexStateSequenceNumberIndex, DbIndexStateSequenceNumberIndexPath, { unique: false });
- const indexEntryStore = db.createObjectStore(DbIndexEntryStore, {
- keyPath: DbIndexEntryKeyPath
- });
- indexEntryStore.createIndex(DbIndexEntryDocumentKeyIndex, DbIndexEntryDocumentKeyIndexPath, { unique: false });
- }
- function createDocumentOverlayStore(db) {
- const documentOverlayStore = db.createObjectStore(DbDocumentOverlayStore, {
- keyPath: DbDocumentOverlayKeyPath
- });
- documentOverlayStore.createIndex(DbDocumentOverlayCollectionPathOverlayIndex, DbDocumentOverlayCollectionPathOverlayIndexPath, { unique: false });
- documentOverlayStore.createIndex(DbDocumentOverlayCollectionGroupOverlayIndex, DbDocumentOverlayCollectionGroupOverlayIndexPath, { unique: false });
- }
- function extractKey(remoteDoc) {
- if (remoteDoc.document) {
- return new DocumentKey(ResourcePath.fromString(remoteDoc.document.name).popFirst(5));
- }
- else if (remoteDoc.noDocument) {
- return DocumentKey.fromSegments(remoteDoc.noDocument.path);
- }
- else if (remoteDoc.unknownDocument) {
- return DocumentKey.fromSegments(remoteDoc.unknownDocument.path);
- }
- else {
- return fail();
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const LOG_TAG$c = 'IndexedDbPersistence';
- /**
- * Oldest acceptable age in milliseconds for client metadata before the client
- * is considered inactive and its associated data is garbage collected.
- */
- const MAX_CLIENT_AGE_MS = 30 * 60 * 1000; // 30 minutes
- /**
- * Oldest acceptable metadata age for clients that may participate in the
- * primary lease election. Clients that have not updated their client metadata
- * within 5 seconds are not eligible to receive a primary lease.
- */
- const MAX_PRIMARY_ELIGIBLE_AGE_MS = 5000;
- /**
- * The interval at which clients will update their metadata, including
- * refreshing their primary lease if held or potentially trying to acquire it if
- * not held.
- *
- * Primary clients may opportunistically refresh their metadata earlier
- * if they're already performing an IndexedDB operation.
- */
- const CLIENT_METADATA_REFRESH_INTERVAL_MS = 4000;
- /** User-facing error when the primary lease is required but not available. */
- const PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG = 'Failed to obtain exclusive access to the persistence layer. To allow ' +
- 'shared access, multi-tab synchronization has to be enabled in all tabs. ' +
- 'If you are using `experimentalForceOwningTab:true`, make sure that only ' +
- 'one tab has persistence enabled at any given time.';
- const UNSUPPORTED_PLATFORM_ERROR_MSG = 'This platform is either missing IndexedDB or is known to have ' +
- 'an incomplete implementation. Offline persistence has been disabled.';
- // The format of the LocalStorage key that stores zombied client is:
- // firestore_zombie_<persistence_prefix>_<instance_key>
- const ZOMBIED_CLIENTS_KEY_PREFIX = 'firestore_zombie';
- /**
- * The name of the main (and currently only) IndexedDB database. This name is
- * appended to the prefix provided to the IndexedDbPersistence constructor.
- */
- const MAIN_DATABASE = 'main';
- /**
- * An IndexedDB-backed instance of Persistence. Data is stored persistently
- * across sessions.
- *
- * On Web only, the Firestore SDKs support shared access to its persistence
- * layer. This allows multiple browser tabs to read and write to IndexedDb and
- * to synchronize state even without network connectivity. Shared access is
- * currently optional and not enabled unless all clients invoke
- * `enablePersistence()` with `{synchronizeTabs:true}`.
- *
- * In multi-tab mode, if multiple clients are active at the same time, the SDK
- * will designate one client as the “primary client”. An effort is made to pick
- * a visible, network-connected and active client, and this client is
- * responsible for letting other clients know about its presence. The primary
- * client writes a unique client-generated identifier (the client ID) to
- * IndexedDb’s “owner” store every 4 seconds. If the primary client fails to
- * update this entry, another client can acquire the lease and take over as
- * primary.
- *
- * Some persistence operations in the SDK are designated as primary-client only
- * operations. This includes the acknowledgment of mutations and all updates of
- * remote documents. The effects of these operations are written to persistence
- * and then broadcast to other tabs via LocalStorage (see
- * `WebStorageSharedClientState`), which then refresh their state from
- * persistence.
- *
- * Similarly, the primary client listens to notifications sent by secondary
- * clients to discover persistence changes written by secondary clients, such as
- * the addition of new mutations and query targets.
- *
- * If multi-tab is not enabled and another tab already obtained the primary
- * lease, IndexedDbPersistence enters a failed state and all subsequent
- * operations will automatically fail.
- *
- * Additionally, there is an optimization so that when a tab is closed, the
- * primary lease is released immediately (this is especially important to make
- * sure that a refreshed tab is able to immediately re-acquire the primary
- * lease). Unfortunately, IndexedDB cannot be reliably used in window.unload
- * since it is an asynchronous API. So in addition to attempting to give up the
- * lease, the leaseholder writes its client ID to a "zombiedClient" entry in
- * LocalStorage which acts as an indicator that another tab should go ahead and
- * take the primary lease immediately regardless of the current lease timestamp.
- *
- * TODO(b/114226234): Remove `synchronizeTabs` section when multi-tab is no
- * longer optional.
- */
- class IndexedDbPersistence {
- constructor(
- /**
- * Whether to synchronize the in-memory state of multiple tabs and share
- * access to local persistence.
- */
- allowTabSynchronization, persistenceKey, clientId, lruParams, queue, window, document, serializer, sequenceNumberSyncer,
- /**
- * If set to true, forcefully obtains database access. Existing tabs will
- * no longer be able to access IndexedDB.
- */
- forceOwningTab, schemaVersion = SCHEMA_VERSION) {
- this.allowTabSynchronization = allowTabSynchronization;
- this.persistenceKey = persistenceKey;
- this.clientId = clientId;
- this.queue = queue;
- this.window = window;
- this.document = document;
- this.sequenceNumberSyncer = sequenceNumberSyncer;
- this.forceOwningTab = forceOwningTab;
- this.schemaVersion = schemaVersion;
- this.listenSequence = null;
- this._started = false;
- this.isPrimary = false;
- this.networkEnabled = true;
- /** Our window.unload handler, if registered. */
- this.windowUnloadHandler = null;
- this.inForeground = false;
- /** Our 'visibilitychange' listener if registered. */
- this.documentVisibilityHandler = null;
- /** The client metadata refresh task. */
- this.clientMetadataRefresher = null;
- /** The last time we garbage collected the client metadata object store. */
- this.lastGarbageCollectionTime = Number.NEGATIVE_INFINITY;
- /** A listener to notify on primary state changes. */
- this.primaryStateListener = _ => Promise.resolve();
- if (!IndexedDbPersistence.isAvailable()) {
- throw new FirestoreError(Code.UNIMPLEMENTED, UNSUPPORTED_PLATFORM_ERROR_MSG);
- }
- this.referenceDelegate = new IndexedDbLruDelegateImpl(this, lruParams);
- this.dbName = persistenceKey + MAIN_DATABASE;
- this.serializer = new LocalSerializer(serializer);
- this.simpleDb = new SimpleDb(this.dbName, this.schemaVersion, new SchemaConverter(this.serializer));
- this.targetCache = new IndexedDbTargetCache(this.referenceDelegate, this.serializer);
- this.remoteDocumentCache = newIndexedDbRemoteDocumentCache(this.serializer);
- this.bundleCache = new IndexedDbBundleCache();
- if (this.window && this.window.localStorage) {
- this.webStorage = this.window.localStorage;
- }
- else {
- this.webStorage = null;
- if (forceOwningTab === false) {
- logError(LOG_TAG$c, 'LocalStorage is unavailable. As a result, persistence may not work ' +
- 'reliably. In particular enablePersistence() could fail immediately ' +
- 'after refreshing the page.');
- }
- }
- }
- /**
- * Attempt to start IndexedDb persistence.
- *
- * @returns Whether persistence was enabled.
- */
- start() {
- // NOTE: This is expected to fail sometimes (in the case of another tab
- // already having the persistence lock), so it's the first thing we should
- // do.
- return this.updateClientMetadataAndTryBecomePrimary()
- .then(() => {
- if (!this.isPrimary && !this.allowTabSynchronization) {
- // Fail `start()` if `synchronizeTabs` is disabled and we cannot
- // obtain the primary lease.
- throw new FirestoreError(Code.FAILED_PRECONDITION, PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG);
- }
- this.attachVisibilityHandler();
- this.attachWindowUnloadHook();
- this.scheduleClientMetadataAndPrimaryLeaseRefreshes();
- return this.runTransaction('getHighestListenSequenceNumber', 'readonly', txn => this.targetCache.getHighestSequenceNumber(txn));
- })
- .then(highestListenSequenceNumber => {
- this.listenSequence = new ListenSequence(highestListenSequenceNumber, this.sequenceNumberSyncer);
- })
- .then(() => {
- this._started = true;
- })
- .catch(reason => {
- this.simpleDb && this.simpleDb.close();
- return Promise.reject(reason);
- });
- }
- /**
- * Registers a listener that gets called when the primary state of the
- * instance changes. Upon registering, this listener is invoked immediately
- * with the current primary state.
- *
- * PORTING NOTE: This is only used for Web multi-tab.
- */
- setPrimaryStateListener(primaryStateListener) {
- this.primaryStateListener = async (primaryState) => {
- if (this.started) {
- return primaryStateListener(primaryState);
- }
- };
- return primaryStateListener(this.isPrimary);
- }
- /**
- * Registers a listener that gets called when the database receives a
- * version change event indicating that it has deleted.
- *
- * PORTING NOTE: This is only used for Web multi-tab.
- */
- setDatabaseDeletedListener(databaseDeletedListener) {
- this.simpleDb.setVersionChangeListener(async (event) => {
- // Check if an attempt is made to delete IndexedDB.
- if (event.newVersion === null) {
- await databaseDeletedListener();
- }
- });
- }
- /**
- * Adjusts the current network state in the client's metadata, potentially
- * affecting the primary lease.
- *
- * PORTING NOTE: This is only used for Web multi-tab.
- */
- setNetworkEnabled(networkEnabled) {
- if (this.networkEnabled !== networkEnabled) {
- this.networkEnabled = networkEnabled;
- // Schedule a primary lease refresh for immediate execution. The eventual
- // lease update will be propagated via `primaryStateListener`.
- this.queue.enqueueAndForget(async () => {
- if (this.started) {
- await this.updateClientMetadataAndTryBecomePrimary();
- }
- });
- }
- }
- /**
- * Updates the client metadata in IndexedDb and attempts to either obtain or
- * extend the primary lease for the local client. Asynchronously notifies the
- * primary state listener if the client either newly obtained or released its
- * primary lease.
- */
- updateClientMetadataAndTryBecomePrimary() {
- return this.runTransaction('updateClientMetadataAndTryBecomePrimary', 'readwrite', txn => {
- const metadataStore = clientMetadataStore(txn);
- return metadataStore
- .put({
- clientId: this.clientId,
- updateTimeMs: Date.now(),
- networkEnabled: this.networkEnabled,
- inForeground: this.inForeground
- })
- .next(() => {
- if (this.isPrimary) {
- return this.verifyPrimaryLease(txn).next(success => {
- if (!success) {
- this.isPrimary = false;
- this.queue.enqueueRetryable(() => this.primaryStateListener(false));
- }
- });
- }
- })
- .next(() => this.canActAsPrimary(txn))
- .next(canActAsPrimary => {
- if (this.isPrimary && !canActAsPrimary) {
- return this.releasePrimaryLeaseIfHeld(txn).next(() => false);
- }
- else if (canActAsPrimary) {
- return this.acquireOrExtendPrimaryLease(txn).next(() => true);
- }
- else {
- return /* canActAsPrimary= */ false;
- }
- });
- })
- .catch(e => {
- if (isIndexedDbTransactionError(e)) {
- logDebug(LOG_TAG$c, 'Failed to extend owner lease: ', e);
- // Proceed with the existing state. Any subsequent access to
- // IndexedDB will verify the lease.
- return this.isPrimary;
- }
- if (!this.allowTabSynchronization) {
- throw e;
- }
- logDebug(LOG_TAG$c, 'Releasing owner lease after error during lease refresh', e);
- return /* isPrimary= */ false;
- })
- .then(isPrimary => {
- if (this.isPrimary !== isPrimary) {
- this.queue.enqueueRetryable(() => this.primaryStateListener(isPrimary));
- }
- this.isPrimary = isPrimary;
- });
- }
- verifyPrimaryLease(txn) {
- const store = primaryClientStore(txn);
- return store.get(DbPrimaryClientKey).next(primaryClient => {
- return PersistencePromise.resolve(this.isLocalClient(primaryClient));
- });
- }
- removeClientMetadata(txn) {
- const metadataStore = clientMetadataStore(txn);
- return metadataStore.delete(this.clientId);
- }
- /**
- * If the garbage collection threshold has passed, prunes the
- * RemoteDocumentChanges and the ClientMetadata store based on the last update
- * time of all clients.
- */
- async maybeGarbageCollectMultiClientState() {
- if (this.isPrimary &&
- !this.isWithinAge(this.lastGarbageCollectionTime, MAX_CLIENT_AGE_MS)) {
- this.lastGarbageCollectionTime = Date.now();
- const inactiveClients = await this.runTransaction('maybeGarbageCollectMultiClientState', 'readwrite-primary', txn => {
- const metadataStore = getStore(txn, DbClientMetadataStore);
- return metadataStore.loadAll().next(existingClients => {
- const active = this.filterActiveClients(existingClients, MAX_CLIENT_AGE_MS);
- const inactive = existingClients.filter(client => active.indexOf(client) === -1);
- // Delete metadata for clients that are no longer considered active.
- return PersistencePromise.forEach(inactive, (inactiveClient) => metadataStore.delete(inactiveClient.clientId)).next(() => inactive);
- });
- }).catch(() => {
- // Ignore primary lease violations or any other type of error. The next
- // primary will run `maybeGarbageCollectMultiClientState()` again.
- // We don't use `ignoreIfPrimaryLeaseLoss()` since we don't want to depend
- // on LocalStore.
- return [];
- });
- // Delete potential leftover entries that may continue to mark the
- // inactive clients as zombied in LocalStorage.
- // Ideally we'd delete the IndexedDb and LocalStorage zombie entries for
- // the client atomically, but we can't. So we opt to delete the IndexedDb
- // entries first to avoid potentially reviving a zombied client.
- if (this.webStorage) {
- for (const inactiveClient of inactiveClients) {
- this.webStorage.removeItem(this.zombiedClientLocalStorageKey(inactiveClient.clientId));
- }
- }
- }
- }
- /**
- * Schedules a recurring timer to update the client metadata and to either
- * extend or acquire the primary lease if the client is eligible.
- */
- scheduleClientMetadataAndPrimaryLeaseRefreshes() {
- this.clientMetadataRefresher = this.queue.enqueueAfterDelay("client_metadata_refresh" /* TimerId.ClientMetadataRefresh */, CLIENT_METADATA_REFRESH_INTERVAL_MS, () => {
- return this.updateClientMetadataAndTryBecomePrimary()
- .then(() => this.maybeGarbageCollectMultiClientState())
- .then(() => this.scheduleClientMetadataAndPrimaryLeaseRefreshes());
- });
- }
- /** Checks whether `client` is the local client. */
- isLocalClient(client) {
- return client ? client.ownerId === this.clientId : false;
- }
- /**
- * Evaluate the state of all active clients and determine whether the local
- * client is or can act as the holder of the primary lease. Returns whether
- * the client is eligible for the lease, but does not actually acquire it.
- * May return 'false' even if there is no active leaseholder and another
- * (foreground) client should become leaseholder instead.
- */
- canActAsPrimary(txn) {
- if (this.forceOwningTab) {
- return PersistencePromise.resolve(true);
- }
- const store = primaryClientStore(txn);
- return store
- .get(DbPrimaryClientKey)
- .next(currentPrimary => {
- const currentLeaseIsValid = currentPrimary !== null &&
- this.isWithinAge(currentPrimary.leaseTimestampMs, MAX_PRIMARY_ELIGIBLE_AGE_MS) &&
- !this.isClientZombied(currentPrimary.ownerId);
- // A client is eligible for the primary lease if:
- // - its network is enabled and the client's tab is in the foreground.
- // - its network is enabled and no other client's tab is in the
- // foreground.
- // - every clients network is disabled and the client's tab is in the
- // foreground.
- // - every clients network is disabled and no other client's tab is in
- // the foreground.
- // - the `forceOwningTab` setting was passed in.
- if (currentLeaseIsValid) {
- if (this.isLocalClient(currentPrimary) && this.networkEnabled) {
- return true;
- }
- if (!this.isLocalClient(currentPrimary)) {
- if (!currentPrimary.allowTabSynchronization) {
- // Fail the `canActAsPrimary` check if the current leaseholder has
- // not opted into multi-tab synchronization. If this happens at
- // client startup, we reject the Promise returned by
- // `enablePersistence()` and the user can continue to use Firestore
- // with in-memory persistence.
- // If this fails during a lease refresh, we will instead block the
- // AsyncQueue from executing further operations. Note that this is
- // acceptable since mixing & matching different `synchronizeTabs`
- // settings is not supported.
- //
- // TODO(b/114226234): Remove this check when `synchronizeTabs` can
- // no longer be turned off.
- throw new FirestoreError(Code.FAILED_PRECONDITION, PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG);
- }
- return false;
- }
- }
- if (this.networkEnabled && this.inForeground) {
- return true;
- }
- return clientMetadataStore(txn)
- .loadAll()
- .next(existingClients => {
- // Process all existing clients and determine whether at least one of
- // them is better suited to obtain the primary lease.
- const preferredCandidate = this.filterActiveClients(existingClients, MAX_PRIMARY_ELIGIBLE_AGE_MS).find(otherClient => {
- if (this.clientId !== otherClient.clientId) {
- const otherClientHasBetterNetworkState = !this.networkEnabled && otherClient.networkEnabled;
- const otherClientHasBetterVisibility = !this.inForeground && otherClient.inForeground;
- const otherClientHasSameNetworkState = this.networkEnabled === otherClient.networkEnabled;
- if (otherClientHasBetterNetworkState ||
- (otherClientHasBetterVisibility &&
- otherClientHasSameNetworkState)) {
- return true;
- }
- }
- return false;
- });
- return preferredCandidate === undefined;
- });
- })
- .next(canActAsPrimary => {
- if (this.isPrimary !== canActAsPrimary) {
- logDebug(LOG_TAG$c, `Client ${canActAsPrimary ? 'is' : 'is not'} eligible for a primary lease.`);
- }
- return canActAsPrimary;
- });
- }
- async shutdown() {
- // The shutdown() operations are idempotent and can be called even when
- // start() aborted (e.g. because it couldn't acquire the persistence lease).
- this._started = false;
- this.markClientZombied();
- if (this.clientMetadataRefresher) {
- this.clientMetadataRefresher.cancel();
- this.clientMetadataRefresher = null;
- }
- this.detachVisibilityHandler();
- this.detachWindowUnloadHook();
- // Use `SimpleDb.runTransaction` directly to avoid failing if another tab
- // has obtained the primary lease.
- await this.simpleDb.runTransaction('shutdown', 'readwrite', [DbPrimaryClientStore, DbClientMetadataStore], simpleDbTxn => {
- const persistenceTransaction = new IndexedDbTransaction(simpleDbTxn, ListenSequence.INVALID);
- return this.releasePrimaryLeaseIfHeld(persistenceTransaction).next(() => this.removeClientMetadata(persistenceTransaction));
- });
- this.simpleDb.close();
- // Remove the entry marking the client as zombied from LocalStorage since
- // we successfully deleted its metadata from IndexedDb.
- this.removeClientZombiedEntry();
- }
- /**
- * Returns clients that are not zombied and have an updateTime within the
- * provided threshold.
- */
- filterActiveClients(clients, activityThresholdMs) {
- return clients.filter(client => this.isWithinAge(client.updateTimeMs, activityThresholdMs) &&
- !this.isClientZombied(client.clientId));
- }
- /**
- * Returns the IDs of the clients that are currently active. If multi-tab
- * is not supported, returns an array that only contains the local client's
- * ID.
- *
- * PORTING NOTE: This is only used for Web multi-tab.
- */
- getActiveClients() {
- return this.runTransaction('getActiveClients', 'readonly', txn => {
- return clientMetadataStore(txn)
- .loadAll()
- .next(clients => this.filterActiveClients(clients, MAX_CLIENT_AGE_MS).map(clientMetadata => clientMetadata.clientId));
- });
- }
- get started() {
- return this._started;
- }
- getMutationQueue(user, indexManager) {
- return IndexedDbMutationQueue.forUser(user, this.serializer, indexManager, this.referenceDelegate);
- }
- getTargetCache() {
- return this.targetCache;
- }
- getRemoteDocumentCache() {
- return this.remoteDocumentCache;
- }
- getIndexManager(user) {
- return new IndexedDbIndexManager(user, this.serializer.remoteSerializer.databaseId);
- }
- getDocumentOverlayCache(user) {
- return IndexedDbDocumentOverlayCache.forUser(this.serializer, user);
- }
- getBundleCache() {
- return this.bundleCache;
- }
- runTransaction(action, mode, transactionOperation) {
- logDebug(LOG_TAG$c, 'Starting transaction:', action);
- const simpleDbMode = mode === 'readonly' ? 'readonly' : 'readwrite';
- const objectStores = getObjectStores(this.schemaVersion);
- let persistenceTransaction;
- // Do all transactions as readwrite against all object stores, since we
- // are the only reader/writer.
- return this.simpleDb
- .runTransaction(action, simpleDbMode, objectStores, simpleDbTxn => {
- persistenceTransaction = new IndexedDbTransaction(simpleDbTxn, this.listenSequence
- ? this.listenSequence.next()
- : ListenSequence.INVALID);
- if (mode === 'readwrite-primary') {
- // While we merely verify that we have (or can acquire) the lease
- // immediately, we wait to extend the primary lease until after
- // executing transactionOperation(). This ensures that even if the
- // transactionOperation takes a long time, we'll use a recent
- // leaseTimestampMs in the extended (or newly acquired) lease.
- return this.verifyPrimaryLease(persistenceTransaction)
- .next(holdsPrimaryLease => {
- if (holdsPrimaryLease) {
- return /* holdsPrimaryLease= */ true;
- }
- return this.canActAsPrimary(persistenceTransaction);
- })
- .next(holdsPrimaryLease => {
- if (!holdsPrimaryLease) {
- logError(`Failed to obtain primary lease for action '${action}'.`);
- this.isPrimary = false;
- this.queue.enqueueRetryable(() => this.primaryStateListener(false));
- throw new FirestoreError(Code.FAILED_PRECONDITION, PRIMARY_LEASE_LOST_ERROR_MSG);
- }
- return transactionOperation(persistenceTransaction);
- })
- .next(result => {
- return this.acquireOrExtendPrimaryLease(persistenceTransaction).next(() => result);
- });
- }
- else {
- return this.verifyAllowTabSynchronization(persistenceTransaction).next(() => transactionOperation(persistenceTransaction));
- }
- })
- .then(result => {
- persistenceTransaction.raiseOnCommittedEvent();
- return result;
- });
- }
- /**
- * Verifies that the current tab is the primary leaseholder or alternatively
- * that the leaseholder has opted into multi-tab synchronization.
- */
- // TODO(b/114226234): Remove this check when `synchronizeTabs` can no longer
- // be turned off.
- verifyAllowTabSynchronization(txn) {
- const store = primaryClientStore(txn);
- return store.get(DbPrimaryClientKey).next(currentPrimary => {
- const currentLeaseIsValid = currentPrimary !== null &&
- this.isWithinAge(currentPrimary.leaseTimestampMs, MAX_PRIMARY_ELIGIBLE_AGE_MS) &&
- !this.isClientZombied(currentPrimary.ownerId);
- if (currentLeaseIsValid && !this.isLocalClient(currentPrimary)) {
- if (!this.forceOwningTab &&
- (!this.allowTabSynchronization ||
- !currentPrimary.allowTabSynchronization)) {
- throw new FirestoreError(Code.FAILED_PRECONDITION, PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG);
- }
- }
- });
- }
- /**
- * Obtains or extends the new primary lease for the local client. This
- * method does not verify that the client is eligible for this lease.
- */
- acquireOrExtendPrimaryLease(txn) {
- const newPrimary = {
- ownerId: this.clientId,
- allowTabSynchronization: this.allowTabSynchronization,
- leaseTimestampMs: Date.now()
- };
- return primaryClientStore(txn).put(DbPrimaryClientKey, newPrimary);
- }
- static isAvailable() {
- return SimpleDb.isAvailable();
- }
- /** Checks the primary lease and removes it if we are the current primary. */
- releasePrimaryLeaseIfHeld(txn) {
- const store = primaryClientStore(txn);
- return store.get(DbPrimaryClientKey).next(primaryClient => {
- if (this.isLocalClient(primaryClient)) {
- logDebug(LOG_TAG$c, 'Releasing primary lease.');
- return store.delete(DbPrimaryClientKey);
- }
- else {
- return PersistencePromise.resolve();
- }
- });
- }
- /** Verifies that `updateTimeMs` is within `maxAgeMs`. */
- isWithinAge(updateTimeMs, maxAgeMs) {
- const now = Date.now();
- const minAcceptable = now - maxAgeMs;
- const maxAcceptable = now;
- if (updateTimeMs < minAcceptable) {
- return false;
- }
- else if (updateTimeMs > maxAcceptable) {
- logError(`Detected an update time that is in the future: ${updateTimeMs} > ${maxAcceptable}`);
- return false;
- }
- return true;
- }
- attachVisibilityHandler() {
- if (this.document !== null &&
- typeof this.document.addEventListener === 'function') {
- this.documentVisibilityHandler = () => {
- this.queue.enqueueAndForget(() => {
- this.inForeground = this.document.visibilityState === 'visible';
- return this.updateClientMetadataAndTryBecomePrimary();
- });
- };
- this.document.addEventListener('visibilitychange', this.documentVisibilityHandler);
- this.inForeground = this.document.visibilityState === 'visible';
- }
- }
- detachVisibilityHandler() {
- if (this.documentVisibilityHandler) {
- this.document.removeEventListener('visibilitychange', this.documentVisibilityHandler);
- this.documentVisibilityHandler = null;
- }
- }
- /**
- * Attaches a window.unload handler that will synchronously write our
- * clientId to a "zombie client id" location in LocalStorage. This can be used
- * by tabs trying to acquire the primary lease to determine that the lease
- * is no longer valid even if the timestamp is recent. This is particularly
- * important for the refresh case (so the tab correctly re-acquires the
- * primary lease). LocalStorage is used for this rather than IndexedDb because
- * it is a synchronous API and so can be used reliably from an unload
- * handler.
- */
- attachWindowUnloadHook() {
- var _a;
- if (typeof ((_a = this.window) === null || _a === void 0 ? void 0 : _a.addEventListener) === 'function') {
- this.windowUnloadHandler = () => {
- // Note: In theory, this should be scheduled on the AsyncQueue since it
- // accesses internal state. We execute this code directly during shutdown
- // to make sure it gets a chance to run.
- this.markClientZombied();
- const safariIndexdbBugVersionRegex = /(?:Version|Mobile)\/1[456]/;
- if (util.isSafari() &&
- (navigator.appVersion.match(safariIndexdbBugVersionRegex) ||
- navigator.userAgent.match(safariIndexdbBugVersionRegex))) {
- // On Safari 14, 15, and 16, we do not run any cleanup actions as it might
- // trigger a bug that prevents Safari from re-opening IndexedDB during
- // the next page load.
- // See https://bugs.webkit.org/show_bug.cgi?id=226547
- this.queue.enterRestrictedMode(/* purgeExistingTasks= */ true);
- }
- this.queue.enqueueAndForget(() => {
- // Attempt graceful shutdown (including releasing our primary lease),
- // but there's no guarantee it will complete.
- return this.shutdown();
- });
- };
- this.window.addEventListener('pagehide', this.windowUnloadHandler);
- }
- }
- detachWindowUnloadHook() {
- if (this.windowUnloadHandler) {
- this.window.removeEventListener('pagehide', this.windowUnloadHandler);
- this.windowUnloadHandler = null;
- }
- }
- /**
- * Returns whether a client is "zombied" based on its LocalStorage entry.
- * Clients become zombied when their tab closes without running all of the
- * cleanup logic in `shutdown()`.
- */
- isClientZombied(clientId) {
- var _a;
- try {
- const isZombied = ((_a = this.webStorage) === null || _a === void 0 ? void 0 : _a.getItem(this.zombiedClientLocalStorageKey(clientId))) !== null;
- logDebug(LOG_TAG$c, `Client '${clientId}' ${isZombied ? 'is' : 'is not'} zombied in LocalStorage`);
- return isZombied;
- }
- catch (e) {
- // Gracefully handle if LocalStorage isn't working.
- logError(LOG_TAG$c, 'Failed to get zombied client id.', e);
- return false;
- }
- }
- /**
- * Record client as zombied (a client that had its tab closed). Zombied
- * clients are ignored during primary tab selection.
- */
- markClientZombied() {
- if (!this.webStorage) {
- return;
- }
- try {
- this.webStorage.setItem(this.zombiedClientLocalStorageKey(this.clientId), String(Date.now()));
- }
- catch (e) {
- // Gracefully handle if LocalStorage isn't available / working.
- logError('Failed to set zombie client id.', e);
- }
- }
- /** Removes the zombied client entry if it exists. */
- removeClientZombiedEntry() {
- if (!this.webStorage) {
- return;
- }
- try {
- this.webStorage.removeItem(this.zombiedClientLocalStorageKey(this.clientId));
- }
- catch (e) {
- // Ignore
- }
- }
- zombiedClientLocalStorageKey(clientId) {
- return `${ZOMBIED_CLIENTS_KEY_PREFIX}_${this.persistenceKey}_${clientId}`;
- }
- }
- /**
- * Helper to get a typed SimpleDbStore for the primary client object store.
- */
- function primaryClientStore(txn) {
- return getStore(txn, DbPrimaryClientStore);
- }
- /**
- * Helper to get a typed SimpleDbStore for the client metadata object store.
- */
- function clientMetadataStore(txn) {
- return getStore(txn, DbClientMetadataStore);
- }
- /**
- * Generates a string used as a prefix when storing data in IndexedDB and
- * LocalStorage.
- */
- function indexedDbStoragePrefix(databaseId, persistenceKey) {
- // Use two different prefix formats:
- //
- // * firestore / persistenceKey / projectID . databaseID / ...
- // * firestore / persistenceKey / projectID / ...
- //
- // projectIDs are DNS-compatible names and cannot contain dots
- // so there's no danger of collisions.
- let database = databaseId.projectId;
- if (!databaseId.isDefaultDatabase) {
- database += '.' + databaseId.database;
- }
- return 'firestore/' + persistenceKey + '/' + database + '/';
- }
- async function indexedDbClearPersistence(persistenceKey) {
- if (!SimpleDb.isAvailable()) {
- return Promise.resolve();
- }
- const dbName = persistenceKey + MAIN_DATABASE;
- await SimpleDb.delete(dbName);
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Compares two array for equality using comparator. The method computes the
- * intersection and invokes `onAdd` for every element that is in `after` but not
- * `before`. `onRemove` is invoked for every element in `before` but missing
- * from `after`.
- *
- * The method creates a copy of both `before` and `after` and runs in O(n log
- * n), where n is the size of the two lists.
- *
- * @param before - The elements that exist in the original array.
- * @param after - The elements to diff against the original array.
- * @param comparator - The comparator for the elements in before and after.
- * @param onAdd - A function to invoke for every element that is part of `
- * after` but not `before`.
- * @param onRemove - A function to invoke for every element that is part of
- * `before` but not `after`.
- */
- function diffArrays(before, after, comparator, onAdd, onRemove) {
- before = [...before];
- after = [...after];
- before.sort(comparator);
- after.sort(comparator);
- const bLen = before.length;
- const aLen = after.length;
- let a = 0;
- let b = 0;
- while (a < aLen && b < bLen) {
- const cmp = comparator(before[b], after[a]);
- if (cmp < 0) {
- // The element was removed if the next element in our ordered
- // walkthrough is only in `before`.
- onRemove(before[b++]);
- }
- else if (cmp > 0) {
- // The element was added if the next element in our ordered walkthrough
- // is only in `after`.
- onAdd(after[a++]);
- }
- else {
- a++;
- b++;
- }
- }
- while (a < aLen) {
- onAdd(after[a++]);
- }
- while (b < bLen) {
- onRemove(before[b++]);
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const LOG_TAG$b = 'LocalStore';
- /**
- * The maximum time to leave a resume token buffered without writing it out.
- * This value is arbitrary: it's long enough to avoid several writes
- * (possibly indefinitely if updates come more frequently than this) but
- * short enough that restarting after crashing will still have a pretty
- * recent resume token.
- */
- const RESUME_TOKEN_MAX_AGE_MICROS = 5 * 60 * 1e6;
- /**
- * Implements `LocalStore` interface.
- *
- * Note: some field defined in this class might have public access level, but
- * the class is not exported so they are only accessible from this module.
- * This is useful to implement optional features (like bundles) in free
- * functions, such that they are tree-shakeable.
- */
- class LocalStoreImpl {
- constructor(
- /** Manages our in-memory or durable persistence. */
- persistence, queryEngine, initialUser, serializer) {
- this.persistence = persistence;
- this.queryEngine = queryEngine;
- this.serializer = serializer;
- /**
- * Maps a targetID to data about its target.
- *
- * PORTING NOTE: We are using an immutable data structure on Web to make re-runs
- * of `applyRemoteEvent()` idempotent.
- */
- this.targetDataByTarget = new SortedMap(primitiveComparator);
- /** Maps a target to its targetID. */
- // TODO(wuandy): Evaluate if TargetId can be part of Target.
- this.targetIdByTarget = new ObjectMap(t => canonifyTarget(t), targetEquals);
- /**
- * A per collection group index of the last read time processed by
- * `getNewDocumentChanges()`.
- *
- * PORTING NOTE: This is only used for multi-tab synchronization.
- */
- this.collectionGroupReadTime = new Map();
- this.remoteDocuments = persistence.getRemoteDocumentCache();
- this.targetCache = persistence.getTargetCache();
- this.bundleCache = persistence.getBundleCache();
- this.initializeUserComponents(initialUser);
- }
- initializeUserComponents(user) {
- // TODO(indexing): Add spec tests that test these components change after a
- // user change
- this.documentOverlayCache = this.persistence.getDocumentOverlayCache(user);
- this.indexManager = this.persistence.getIndexManager(user);
- this.mutationQueue = this.persistence.getMutationQueue(user, this.indexManager);
- this.localDocuments = new LocalDocumentsView(this.remoteDocuments, this.mutationQueue, this.documentOverlayCache, this.indexManager);
- this.remoteDocuments.setIndexManager(this.indexManager);
- this.queryEngine.initialize(this.localDocuments, this.indexManager);
- }
- collectGarbage(garbageCollector) {
- return this.persistence.runTransaction('Collect garbage', 'readwrite-primary', txn => garbageCollector.collect(txn, this.targetDataByTarget));
- }
- }
- function newLocalStore(
- /** Manages our in-memory or durable persistence. */
- persistence, queryEngine, initialUser, serializer) {
- return new LocalStoreImpl(persistence, queryEngine, initialUser, serializer);
- }
- /**
- * Tells the LocalStore that the currently authenticated user has changed.
- *
- * In response the local store switches the mutation queue to the new user and
- * returns any resulting document changes.
- */
- // PORTING NOTE: Android and iOS only return the documents affected by the
- // change.
- async function localStoreHandleUserChange(localStore, user) {
- const localStoreImpl = debugCast(localStore);
- const result = await localStoreImpl.persistence.runTransaction('Handle user change', 'readonly', txn => {
- // Swap out the mutation queue, grabbing the pending mutation batches
- // before and after.
- let oldBatches;
- return localStoreImpl.mutationQueue
- .getAllMutationBatches(txn)
- .next(promisedOldBatches => {
- oldBatches = promisedOldBatches;
- localStoreImpl.initializeUserComponents(user);
- return localStoreImpl.mutationQueue.getAllMutationBatches(txn);
- })
- .next(newBatches => {
- const removedBatchIds = [];
- const addedBatchIds = [];
- // Union the old/new changed keys.
- let changedKeys = documentKeySet();
- for (const batch of oldBatches) {
- removedBatchIds.push(batch.batchId);
- for (const mutation of batch.mutations) {
- changedKeys = changedKeys.add(mutation.key);
- }
- }
- for (const batch of newBatches) {
- addedBatchIds.push(batch.batchId);
- for (const mutation of batch.mutations) {
- changedKeys = changedKeys.add(mutation.key);
- }
- }
- // Return the set of all (potentially) changed documents and the list
- // of mutation batch IDs that were affected by change.
- return localStoreImpl.localDocuments
- .getDocuments(txn, changedKeys)
- .next(affectedDocuments => {
- return {
- affectedDocuments,
- removedBatchIds,
- addedBatchIds
- };
- });
- });
- });
- return result;
- }
- /* Accepts locally generated Mutations and commit them to storage. */
- function localStoreWriteLocally(localStore, mutations) {
- const localStoreImpl = debugCast(localStore);
- const localWriteTime = Timestamp.now();
- const keys = mutations.reduce((keys, m) => keys.add(m.key), documentKeySet());
- let overlayedDocuments;
- let mutationBatch;
- return localStoreImpl.persistence
- .runTransaction('Locally write mutations', 'readwrite', txn => {
- // Figure out which keys do not have a remote version in the cache, this
- // is needed to create the right overlay mutation: if no remote version
- // presents, we do not need to create overlays as patch mutations.
- // TODO(Overlay): Is there a better way to determine this? Using the
- // document version does not work because local mutations set them back
- // to 0.
- let remoteDocs = mutableDocumentMap();
- let docsWithoutRemoteVersion = documentKeySet();
- return localStoreImpl.remoteDocuments
- .getEntries(txn, keys)
- .next(docs => {
- remoteDocs = docs;
- remoteDocs.forEach((key, doc) => {
- if (!doc.isValidDocument()) {
- docsWithoutRemoteVersion = docsWithoutRemoteVersion.add(key);
- }
- });
- })
- .next(() => {
- // Load and apply all existing mutations. This lets us compute the
- // current base state for all non-idempotent transforms before applying
- // any additional user-provided writes.
- return localStoreImpl.localDocuments.getOverlayedDocuments(txn, remoteDocs);
- })
- .next((docs) => {
- overlayedDocuments = docs;
- // For non-idempotent mutations (such as `FieldValue.increment()`),
- // we record the base state in a separate patch mutation. This is
- // later used to guarantee consistent values and prevents flicker
- // even if the backend sends us an update that already includes our
- // transform.
- const baseMutations = [];
- for (const mutation of mutations) {
- const baseValue = mutationExtractBaseValue(mutation, overlayedDocuments.get(mutation.key).overlayedDocument);
- if (baseValue != null) {
- // NOTE: The base state should only be applied if there's some
- // existing document to override, so use a Precondition of
- // exists=true
- baseMutations.push(new PatchMutation(mutation.key, baseValue, extractFieldMask(baseValue.value.mapValue), Precondition.exists(true)));
- }
- }
- return localStoreImpl.mutationQueue.addMutationBatch(txn, localWriteTime, baseMutations, mutations);
- })
- .next(batch => {
- mutationBatch = batch;
- const overlays = batch.applyToLocalDocumentSet(overlayedDocuments, docsWithoutRemoteVersion);
- return localStoreImpl.documentOverlayCache.saveOverlays(txn, batch.batchId, overlays);
- });
- })
- .then(() => ({
- batchId: mutationBatch.batchId,
- changes: convertOverlayedDocumentMapToDocumentMap(overlayedDocuments)
- }));
- }
- /**
- * Acknowledges the given batch.
- *
- * On the happy path when a batch is acknowledged, the local store will
- *
- * + remove the batch from the mutation queue;
- * + apply the changes to the remote document cache;
- * + recalculate the latency compensated view implied by those changes (there
- * may be mutations in the queue that affect the documents but haven't been
- * acknowledged yet); and
- * + give the changed documents back the sync engine
- *
- * @returns The resulting (modified) documents.
- */
- function localStoreAcknowledgeBatch(localStore, batchResult) {
- const localStoreImpl = debugCast(localStore);
- return localStoreImpl.persistence.runTransaction('Acknowledge batch', 'readwrite-primary', txn => {
- const affected = batchResult.batch.keys();
- const documentBuffer = localStoreImpl.remoteDocuments.newChangeBuffer({
- trackRemovals: true // Make sure document removals show up in `getNewDocumentChanges()`
- });
- return applyWriteToRemoteDocuments(localStoreImpl, txn, batchResult, documentBuffer)
- .next(() => documentBuffer.apply(txn))
- .next(() => localStoreImpl.mutationQueue.performConsistencyCheck(txn))
- .next(() => localStoreImpl.documentOverlayCache.removeOverlaysForBatchId(txn, affected, batchResult.batch.batchId))
- .next(() => localStoreImpl.localDocuments.recalculateAndSaveOverlaysForDocumentKeys(txn, getKeysWithTransformResults(batchResult)))
- .next(() => localStoreImpl.localDocuments.getDocuments(txn, affected));
- });
- }
- function getKeysWithTransformResults(batchResult) {
- let result = documentKeySet();
- for (let i = 0; i < batchResult.mutationResults.length; ++i) {
- const mutationResult = batchResult.mutationResults[i];
- if (mutationResult.transformResults.length > 0) {
- result = result.add(batchResult.batch.mutations[i].key);
- }
- }
- return result;
- }
- /**
- * Removes mutations from the MutationQueue for the specified batch;
- * LocalDocuments will be recalculated.
- *
- * @returns The resulting modified documents.
- */
- function localStoreRejectBatch(localStore, batchId) {
- const localStoreImpl = debugCast(localStore);
- return localStoreImpl.persistence.runTransaction('Reject batch', 'readwrite-primary', txn => {
- let affectedKeys;
- return localStoreImpl.mutationQueue
- .lookupMutationBatch(txn, batchId)
- .next((batch) => {
- hardAssert(batch !== null);
- affectedKeys = batch.keys();
- return localStoreImpl.mutationQueue.removeMutationBatch(txn, batch);
- })
- .next(() => localStoreImpl.mutationQueue.performConsistencyCheck(txn))
- .next(() => localStoreImpl.documentOverlayCache.removeOverlaysForBatchId(txn, affectedKeys, batchId))
- .next(() => localStoreImpl.localDocuments.recalculateAndSaveOverlaysForDocumentKeys(txn, affectedKeys))
- .next(() => localStoreImpl.localDocuments.getDocuments(txn, affectedKeys));
- });
- }
- /**
- * Returns the largest (latest) batch id in mutation queue that is pending
- * server response.
- *
- * Returns `BATCHID_UNKNOWN` if the queue is empty.
- */
- function localStoreGetHighestUnacknowledgedBatchId(localStore) {
- const localStoreImpl = debugCast(localStore);
- return localStoreImpl.persistence.runTransaction('Get highest unacknowledged batch id', 'readonly', txn => localStoreImpl.mutationQueue.getHighestUnacknowledgedBatchId(txn));
- }
- /**
- * Returns the last consistent snapshot processed (used by the RemoteStore to
- * determine whether to buffer incoming snapshots from the backend).
- */
- function localStoreGetLastRemoteSnapshotVersion(localStore) {
- const localStoreImpl = debugCast(localStore);
- return localStoreImpl.persistence.runTransaction('Get last remote snapshot version', 'readonly', txn => localStoreImpl.targetCache.getLastRemoteSnapshotVersion(txn));
- }
- /**
- * Updates the "ground-state" (remote) documents. We assume that the remote
- * event reflects any write batches that have been acknowledged or rejected
- * (i.e. we do not re-apply local mutations to updates from this event).
- *
- * LocalDocuments are re-calculated if there are remaining mutations in the
- * queue.
- */
- function localStoreApplyRemoteEventToLocalCache(localStore, remoteEvent) {
- const localStoreImpl = debugCast(localStore);
- const remoteVersion = remoteEvent.snapshotVersion;
- let newTargetDataByTargetMap = localStoreImpl.targetDataByTarget;
- return localStoreImpl.persistence
- .runTransaction('Apply remote event', 'readwrite-primary', txn => {
- const documentBuffer = localStoreImpl.remoteDocuments.newChangeBuffer({
- trackRemovals: true // Make sure document removals show up in `getNewDocumentChanges()`
- });
- // Reset newTargetDataByTargetMap in case this transaction gets re-run.
- newTargetDataByTargetMap = localStoreImpl.targetDataByTarget;
- const promises = [];
- remoteEvent.targetChanges.forEach((change, targetId) => {
- const oldTargetData = newTargetDataByTargetMap.get(targetId);
- if (!oldTargetData) {
- return;
- }
- // Only update the remote keys if the target is still active. This
- // ensures that we can persist the updated target data along with
- // the updated assignment.
- promises.push(localStoreImpl.targetCache
- .removeMatchingKeys(txn, change.removedDocuments, targetId)
- .next(() => {
- return localStoreImpl.targetCache.addMatchingKeys(txn, change.addedDocuments, targetId);
- }));
- let newTargetData = oldTargetData.withSequenceNumber(txn.currentSequenceNumber);
- if (remoteEvent.targetMismatches.get(targetId) !== null) {
- newTargetData = newTargetData
- .withResumeToken(ByteString.EMPTY_BYTE_STRING, SnapshotVersion.min())
- .withLastLimboFreeSnapshotVersion(SnapshotVersion.min());
- }
- else if (change.resumeToken.approximateByteSize() > 0) {
- newTargetData = newTargetData.withResumeToken(change.resumeToken, remoteVersion);
- }
- newTargetDataByTargetMap = newTargetDataByTargetMap.insert(targetId, newTargetData);
- // Update the target data if there are target changes (or if
- // sufficient time has passed since the last update).
- if (shouldPersistTargetData(oldTargetData, newTargetData, change)) {
- promises.push(localStoreImpl.targetCache.updateTargetData(txn, newTargetData));
- }
- });
- let changedDocs = mutableDocumentMap();
- let existenceChangedKeys = documentKeySet();
- remoteEvent.documentUpdates.forEach(key => {
- if (remoteEvent.resolvedLimboDocuments.has(key)) {
- promises.push(localStoreImpl.persistence.referenceDelegate.updateLimboDocument(txn, key));
- }
- });
- // Each loop iteration only affects its "own" doc, so it's safe to get all
- // the remote documents in advance in a single call.
- promises.push(populateDocumentChangeBuffer(txn, documentBuffer, remoteEvent.documentUpdates).next(result => {
- changedDocs = result.changedDocuments;
- existenceChangedKeys = result.existenceChangedKeys;
- }));
- // HACK: The only reason we allow a null snapshot version is so that we
- // can synthesize remote events when we get permission denied errors while
- // trying to resolve the state of a locally cached document that is in
- // limbo.
- if (!remoteVersion.isEqual(SnapshotVersion.min())) {
- const updateRemoteVersion = localStoreImpl.targetCache
- .getLastRemoteSnapshotVersion(txn)
- .next(lastRemoteSnapshotVersion => {
- return localStoreImpl.targetCache.setTargetsMetadata(txn, txn.currentSequenceNumber, remoteVersion);
- });
- promises.push(updateRemoteVersion);
- }
- return PersistencePromise.waitFor(promises)
- .next(() => documentBuffer.apply(txn))
- .next(() => localStoreImpl.localDocuments.getLocalViewOfDocuments(txn, changedDocs, existenceChangedKeys))
- .next(() => changedDocs);
- })
- .then(changedDocs => {
- localStoreImpl.targetDataByTarget = newTargetDataByTargetMap;
- return changedDocs;
- });
- }
- /**
- * Populates document change buffer with documents from backend or a bundle.
- * Returns the document changes resulting from applying those documents, and
- * also a set of documents whose existence state are changed as a result.
- *
- * @param txn - Transaction to use to read existing documents from storage.
- * @param documentBuffer - Document buffer to collect the resulted changes to be
- * applied to storage.
- * @param documents - Documents to be applied.
- */
- function populateDocumentChangeBuffer(txn, documentBuffer, documents) {
- let updatedKeys = documentKeySet();
- let existenceChangedKeys = documentKeySet();
- documents.forEach(k => (updatedKeys = updatedKeys.add(k)));
- return documentBuffer.getEntries(txn, updatedKeys).next(existingDocs => {
- let changedDocuments = mutableDocumentMap();
- documents.forEach((key, doc) => {
- const existingDoc = existingDocs.get(key);
- // Check if see if there is a existence state change for this document.
- if (doc.isFoundDocument() !== existingDoc.isFoundDocument()) {
- existenceChangedKeys = existenceChangedKeys.add(key);
- }
- // Note: The order of the steps below is important, since we want
- // to ensure that rejected limbo resolutions (which fabricate
- // NoDocuments with SnapshotVersion.min()) never add documents to
- // cache.
- if (doc.isNoDocument() && doc.version.isEqual(SnapshotVersion.min())) {
- // NoDocuments with SnapshotVersion.min() are used in manufactured
- // events. We remove these documents from cache since we lost
- // access.
- documentBuffer.removeEntry(key, doc.readTime);
- changedDocuments = changedDocuments.insert(key, doc);
- }
- else if (!existingDoc.isValidDocument() ||
- doc.version.compareTo(existingDoc.version) > 0 ||
- (doc.version.compareTo(existingDoc.version) === 0 &&
- existingDoc.hasPendingWrites)) {
- documentBuffer.addEntry(doc);
- changedDocuments = changedDocuments.insert(key, doc);
- }
- else {
- logDebug(LOG_TAG$b, 'Ignoring outdated watch update for ', key, '. Current version:', existingDoc.version, ' Watch version:', doc.version);
- }
- });
- return { changedDocuments, existenceChangedKeys };
- });
- }
- /**
- * Returns true if the newTargetData should be persisted during an update of
- * an active target. TargetData should always be persisted when a target is
- * being released and should not call this function.
- *
- * While the target is active, TargetData updates can be omitted when nothing
- * about the target has changed except metadata like the resume token or
- * snapshot version. Occasionally it's worth the extra write to prevent these
- * values from getting too stale after a crash, but this doesn't have to be
- * too frequent.
- */
- function shouldPersistTargetData(oldTargetData, newTargetData, change) {
- // Always persist target data if we don't already have a resume token.
- if (oldTargetData.resumeToken.approximateByteSize() === 0) {
- return true;
- }
- // Don't allow resume token changes to be buffered indefinitely. This
- // allows us to be reasonably up-to-date after a crash and avoids needing
- // to loop over all active queries on shutdown. Especially in the browser
- // we may not get time to do anything interesting while the current tab is
- // closing.
- const timeDelta = newTargetData.snapshotVersion.toMicroseconds() -
- oldTargetData.snapshotVersion.toMicroseconds();
- if (timeDelta >= RESUME_TOKEN_MAX_AGE_MICROS) {
- return true;
- }
- // Otherwise if the only thing that has changed about a target is its resume
- // token it's not worth persisting. Note that the RemoteStore keeps an
- // in-memory view of the currently active targets which includes the current
- // resume token, so stream failure or user changes will still use an
- // up-to-date resume token regardless of what we do here.
- const changes = change.addedDocuments.size +
- change.modifiedDocuments.size +
- change.removedDocuments.size;
- return changes > 0;
- }
- /**
- * Notifies local store of the changed views to locally pin documents.
- */
- async function localStoreNotifyLocalViewChanges(localStore, viewChanges) {
- const localStoreImpl = debugCast(localStore);
- try {
- await localStoreImpl.persistence.runTransaction('notifyLocalViewChanges', 'readwrite', txn => {
- return PersistencePromise.forEach(viewChanges, (viewChange) => {
- return PersistencePromise.forEach(viewChange.addedKeys, (key) => localStoreImpl.persistence.referenceDelegate.addReference(txn, viewChange.targetId, key)).next(() => PersistencePromise.forEach(viewChange.removedKeys, (key) => localStoreImpl.persistence.referenceDelegate.removeReference(txn, viewChange.targetId, key)));
- });
- });
- }
- catch (e) {
- if (isIndexedDbTransactionError(e)) {
- // If `notifyLocalViewChanges` fails, we did not advance the sequence
- // number for the documents that were included in this transaction.
- // This might trigger them to be deleted earlier than they otherwise
- // would have, but it should not invalidate the integrity of the data.
- logDebug(LOG_TAG$b, 'Failed to update sequence numbers: ' + e);
- }
- else {
- throw e;
- }
- }
- for (const viewChange of viewChanges) {
- const targetId = viewChange.targetId;
- if (!viewChange.fromCache) {
- const targetData = localStoreImpl.targetDataByTarget.get(targetId);
- // Advance the last limbo free snapshot version
- const lastLimboFreeSnapshotVersion = targetData.snapshotVersion;
- const updatedTargetData = targetData.withLastLimboFreeSnapshotVersion(lastLimboFreeSnapshotVersion);
- localStoreImpl.targetDataByTarget =
- localStoreImpl.targetDataByTarget.insert(targetId, updatedTargetData);
- // TODO(b/272564316): Apply the optimization done on other platforms.
- // This is a problem for web because saving the updated targetData from
- // non-primary client conflicts with what primary client saved.
- }
- }
- }
- /**
- * Gets the mutation batch after the passed in batchId in the mutation queue
- * or null if empty.
- * @param afterBatchId - If provided, the batch to search after.
- * @returns The next mutation or null if there wasn't one.
- */
- function localStoreGetNextMutationBatch(localStore, afterBatchId) {
- const localStoreImpl = debugCast(localStore);
- return localStoreImpl.persistence.runTransaction('Get next mutation batch', 'readonly', txn => {
- if (afterBatchId === undefined) {
- afterBatchId = BATCHID_UNKNOWN;
- }
- return localStoreImpl.mutationQueue.getNextMutationBatchAfterBatchId(txn, afterBatchId);
- });
- }
- /**
- * Reads the current value of a Document with a given key or null if not
- * found - used for testing.
- */
- function localStoreReadDocument(localStore, key) {
- const localStoreImpl = debugCast(localStore);
- return localStoreImpl.persistence.runTransaction('read document', 'readonly', txn => localStoreImpl.localDocuments.getDocument(txn, key));
- }
- /**
- * Assigns the given target an internal ID so that its results can be pinned so
- * they don't get GC'd. A target must be allocated in the local store before
- * the store can be used to manage its view.
- *
- * Allocating an already allocated `Target` will return the existing `TargetData`
- * for that `Target`.
- */
- function localStoreAllocateTarget(localStore, target) {
- const localStoreImpl = debugCast(localStore);
- return localStoreImpl.persistence
- .runTransaction('Allocate target', 'readwrite', txn => {
- let targetData;
- return localStoreImpl.targetCache
- .getTargetData(txn, target)
- .next((cached) => {
- if (cached) {
- // This target has been listened to previously, so reuse the
- // previous targetID.
- // TODO(mcg): freshen last accessed date?
- targetData = cached;
- return PersistencePromise.resolve(targetData);
- }
- else {
- return localStoreImpl.targetCache
- .allocateTargetId(txn)
- .next(targetId => {
- targetData = new TargetData(target, targetId, "TargetPurposeListen" /* TargetPurpose.Listen */, txn.currentSequenceNumber);
- return localStoreImpl.targetCache
- .addTargetData(txn, targetData)
- .next(() => targetData);
- });
- }
- });
- })
- .then(targetData => {
- // If Multi-Tab is enabled, the existing target data may be newer than
- // the in-memory data
- const cachedTargetData = localStoreImpl.targetDataByTarget.get(targetData.targetId);
- if (cachedTargetData === null ||
- targetData.snapshotVersion.compareTo(cachedTargetData.snapshotVersion) >
- 0) {
- localStoreImpl.targetDataByTarget =
- localStoreImpl.targetDataByTarget.insert(targetData.targetId, targetData);
- localStoreImpl.targetIdByTarget.set(target, targetData.targetId);
- }
- return targetData;
- });
- }
- /**
- * Returns the TargetData as seen by the LocalStore, including updates that may
- * have not yet been persisted to the TargetCache.
- */
- // Visible for testing.
- function localStoreGetTargetData(localStore, transaction, target) {
- const localStoreImpl = debugCast(localStore);
- const targetId = localStoreImpl.targetIdByTarget.get(target);
- if (targetId !== undefined) {
- return PersistencePromise.resolve(localStoreImpl.targetDataByTarget.get(targetId));
- }
- else {
- return localStoreImpl.targetCache.getTargetData(transaction, target);
- }
- }
- /**
- * Unpins all the documents associated with the given target. If
- * `keepPersistedTargetData` is set to false and Eager GC enabled, the method
- * directly removes the associated target data from the target cache.
- *
- * Releasing a non-existing `Target` is a no-op.
- */
- // PORTING NOTE: `keepPersistedTargetData` is multi-tab only.
- async function localStoreReleaseTarget(localStore, targetId, keepPersistedTargetData) {
- const localStoreImpl = debugCast(localStore);
- const targetData = localStoreImpl.targetDataByTarget.get(targetId);
- const mode = keepPersistedTargetData ? 'readwrite' : 'readwrite-primary';
- try {
- if (!keepPersistedTargetData) {
- await localStoreImpl.persistence.runTransaction('Release target', mode, txn => {
- return localStoreImpl.persistence.referenceDelegate.removeTarget(txn, targetData);
- });
- }
- }
- catch (e) {
- if (isIndexedDbTransactionError(e)) {
- // All `releaseTarget` does is record the final metadata state for the
- // target, but we've been recording this periodically during target
- // activity. If we lose this write this could cause a very slight
- // difference in the order of target deletion during GC, but we
- // don't define exact LRU semantics so this is acceptable.
- logDebug(LOG_TAG$b, `Failed to update sequence numbers for target ${targetId}: ${e}`);
- }
- else {
- throw e;
- }
- }
- localStoreImpl.targetDataByTarget =
- localStoreImpl.targetDataByTarget.remove(targetId);
- localStoreImpl.targetIdByTarget.delete(targetData.target);
- }
- /**
- * Runs the specified query against the local store and returns the results,
- * potentially taking advantage of query data from previous executions (such
- * as the set of remote keys).
- *
- * @param usePreviousResults - Whether results from previous executions can
- * be used to optimize this query execution.
- */
- function localStoreExecuteQuery(localStore, query, usePreviousResults) {
- const localStoreImpl = debugCast(localStore);
- let lastLimboFreeSnapshotVersion = SnapshotVersion.min();
- let remoteKeys = documentKeySet();
- return localStoreImpl.persistence.runTransaction('Execute query', 'readonly', txn => {
- return localStoreGetTargetData(localStoreImpl, txn, queryToTarget(query))
- .next(targetData => {
- if (targetData) {
- lastLimboFreeSnapshotVersion =
- targetData.lastLimboFreeSnapshotVersion;
- return localStoreImpl.targetCache
- .getMatchingKeysForTargetId(txn, targetData.targetId)
- .next(result => {
- remoteKeys = result;
- });
- }
- })
- .next(() => localStoreImpl.queryEngine.getDocumentsMatchingQuery(txn, query, usePreviousResults
- ? lastLimboFreeSnapshotVersion
- : SnapshotVersion.min(), usePreviousResults ? remoteKeys : documentKeySet()))
- .next(documents => {
- setMaxReadTime(localStoreImpl, queryCollectionGroup(query), documents);
- return { documents, remoteKeys };
- });
- });
- }
- function applyWriteToRemoteDocuments(localStoreImpl, txn, batchResult, documentBuffer) {
- const batch = batchResult.batch;
- const docKeys = batch.keys();
- let promiseChain = PersistencePromise.resolve();
- docKeys.forEach(docKey => {
- promiseChain = promiseChain
- .next(() => documentBuffer.getEntry(txn, docKey))
- .next(doc => {
- const ackVersion = batchResult.docVersions.get(docKey);
- hardAssert(ackVersion !== null);
- if (doc.version.compareTo(ackVersion) < 0) {
- batch.applyToRemoteDocument(doc, batchResult);
- if (doc.isValidDocument()) {
- // We use the commitVersion as the readTime rather than the
- // document's updateTime since the updateTime is not advanced
- // for updates that do not modify the underlying document.
- doc.setReadTime(batchResult.commitVersion);
- documentBuffer.addEntry(doc);
- }
- }
- });
- });
- return promiseChain.next(() => localStoreImpl.mutationQueue.removeMutationBatch(txn, batch));
- }
- /** Returns the local view of the documents affected by a mutation batch. */
- // PORTING NOTE: Multi-Tab only.
- function localStoreLookupMutationDocuments(localStore, batchId) {
- const localStoreImpl = debugCast(localStore);
- const mutationQueueImpl = debugCast(localStoreImpl.mutationQueue);
- return localStoreImpl.persistence.runTransaction('Lookup mutation documents', 'readonly', txn => {
- return mutationQueueImpl.lookupMutationKeys(txn, batchId).next(keys => {
- if (keys) {
- return localStoreImpl.localDocuments.getDocuments(txn, keys);
- }
- else {
- return PersistencePromise.resolve(null);
- }
- });
- });
- }
- // PORTING NOTE: Multi-Tab only.
- function localStoreRemoveCachedMutationBatchMetadata(localStore, batchId) {
- const mutationQueueImpl = debugCast(debugCast(localStore, LocalStoreImpl).mutationQueue);
- mutationQueueImpl.removeCachedMutationKeys(batchId);
- }
- // PORTING NOTE: Multi-Tab only.
- function localStoreGetActiveClients(localStore) {
- const persistenceImpl = debugCast(debugCast(localStore, LocalStoreImpl).persistence);
- return persistenceImpl.getActiveClients();
- }
- // PORTING NOTE: Multi-Tab only.
- function localStoreGetCachedTarget(localStore, targetId) {
- const localStoreImpl = debugCast(localStore);
- const targetCacheImpl = debugCast(localStoreImpl.targetCache);
- const cachedTargetData = localStoreImpl.targetDataByTarget.get(targetId);
- if (cachedTargetData) {
- return Promise.resolve(cachedTargetData.target);
- }
- else {
- return localStoreImpl.persistence.runTransaction('Get target data', 'readonly', txn => {
- return targetCacheImpl
- .getTargetDataForTarget(txn, targetId)
- .next(targetData => (targetData ? targetData.target : null));
- });
- }
- }
- /**
- * Returns the set of documents that have been updated since the last call.
- * If this is the first call, returns the set of changes since client
- * initialization. Further invocations will return document that have changed
- * since the prior call.
- */
- // PORTING NOTE: Multi-Tab only.
- function localStoreGetNewDocumentChanges(localStore, collectionGroup) {
- const localStoreImpl = debugCast(localStore);
- // Get the current maximum read time for the collection. This should always
- // exist, but to reduce the chance for regressions we default to
- // SnapshotVersion.Min()
- // TODO(indexing): Consider removing the default value.
- const readTime = localStoreImpl.collectionGroupReadTime.get(collectionGroup) ||
- SnapshotVersion.min();
- return localStoreImpl.persistence
- .runTransaction('Get new document changes', 'readonly', txn => localStoreImpl.remoteDocuments.getAllFromCollectionGroup(txn, collectionGroup, newIndexOffsetSuccessorFromReadTime(readTime, INITIAL_LARGEST_BATCH_ID),
- /* limit= */ Number.MAX_SAFE_INTEGER))
- .then(changedDocs => {
- setMaxReadTime(localStoreImpl, collectionGroup, changedDocs);
- return changedDocs;
- });
- }
- /** Sets the collection group's maximum read time from the given documents. */
- // PORTING NOTE: Multi-Tab only.
- function setMaxReadTime(localStoreImpl, collectionGroup, changedDocs) {
- let readTime = localStoreImpl.collectionGroupReadTime.get(collectionGroup) ||
- SnapshotVersion.min();
- changedDocs.forEach((_, doc) => {
- if (doc.readTime.compareTo(readTime) > 0) {
- readTime = doc.readTime;
- }
- });
- localStoreImpl.collectionGroupReadTime.set(collectionGroup, readTime);
- }
- /**
- * Creates a new target using the given bundle name, which will be used to
- * hold the keys of all documents from the bundle in query-document mappings.
- * This ensures that the loaded documents do not get garbage collected
- * right away.
- */
- function umbrellaTarget(bundleName) {
- // It is OK that the path used for the query is not valid, because this will
- // not be read and queried.
- return queryToTarget(newQueryForPath(ResourcePath.fromString(`__bundle__/docs/${bundleName}`)));
- }
- /**
- * Applies the documents from a bundle to the "ground-state" (remote)
- * documents.
- *
- * LocalDocuments are re-calculated if there are remaining mutations in the
- * queue.
- */
- async function localStoreApplyBundledDocuments(localStore, bundleConverter, documents, bundleName) {
- const localStoreImpl = debugCast(localStore);
- let documentKeys = documentKeySet();
- let documentMap = mutableDocumentMap();
- for (const bundleDoc of documents) {
- const documentKey = bundleConverter.toDocumentKey(bundleDoc.metadata.name);
- if (bundleDoc.document) {
- documentKeys = documentKeys.add(documentKey);
- }
- const doc = bundleConverter.toMutableDocument(bundleDoc);
- doc.setReadTime(bundleConverter.toSnapshotVersion(bundleDoc.metadata.readTime));
- documentMap = documentMap.insert(documentKey, doc);
- }
- const documentBuffer = localStoreImpl.remoteDocuments.newChangeBuffer({
- trackRemovals: true // Make sure document removals show up in `getNewDocumentChanges()`
- });
- // Allocates a target to hold all document keys from the bundle, such that
- // they will not get garbage collected right away.
- const umbrellaTargetData = await localStoreAllocateTarget(localStoreImpl, umbrellaTarget(bundleName));
- return localStoreImpl.persistence.runTransaction('Apply bundle documents', 'readwrite', txn => {
- return populateDocumentChangeBuffer(txn, documentBuffer, documentMap)
- .next(documentChangeResult => {
- documentBuffer.apply(txn);
- return documentChangeResult;
- })
- .next(documentChangeResult => {
- return localStoreImpl.targetCache
- .removeMatchingKeysForTargetId(txn, umbrellaTargetData.targetId)
- .next(() => localStoreImpl.targetCache.addMatchingKeys(txn, documentKeys, umbrellaTargetData.targetId))
- .next(() => localStoreImpl.localDocuments.getLocalViewOfDocuments(txn, documentChangeResult.changedDocuments, documentChangeResult.existenceChangedKeys))
- .next(() => documentChangeResult.changedDocuments);
- });
- });
- }
- /**
- * Returns a promise of a boolean to indicate if the given bundle has already
- * been loaded and the create time is newer than the current loading bundle.
- */
- function localStoreHasNewerBundle(localStore, bundleMetadata) {
- const localStoreImpl = debugCast(localStore);
- const currentReadTime = fromVersion(bundleMetadata.createTime);
- return localStoreImpl.persistence
- .runTransaction('hasNewerBundle', 'readonly', transaction => {
- return localStoreImpl.bundleCache.getBundleMetadata(transaction, bundleMetadata.id);
- })
- .then(cached => {
- return !!cached && cached.createTime.compareTo(currentReadTime) >= 0;
- });
- }
- /**
- * Saves the given `BundleMetadata` to local persistence.
- */
- function localStoreSaveBundle(localStore, bundleMetadata) {
- const localStoreImpl = debugCast(localStore);
- return localStoreImpl.persistence.runTransaction('Save bundle', 'readwrite', transaction => {
- return localStoreImpl.bundleCache.saveBundleMetadata(transaction, bundleMetadata);
- });
- }
- /**
- * Returns a promise of a `NamedQuery` associated with given query name. Promise
- * resolves to undefined if no persisted data can be found.
- */
- function localStoreGetNamedQuery(localStore, queryName) {
- const localStoreImpl = debugCast(localStore);
- return localStoreImpl.persistence.runTransaction('Get named query', 'readonly', transaction => localStoreImpl.bundleCache.getNamedQuery(transaction, queryName));
- }
- /**
- * Saves the given `NamedQuery` to local persistence.
- */
- async function localStoreSaveNamedQuery(localStore, query, documents = documentKeySet()) {
- // Allocate a target for the named query such that it can be resumed
- // from associated read time if users use it to listen.
- // NOTE: this also means if no corresponding target exists, the new target
- // will remain active and will not get collected, unless users happen to
- // unlisten the query somehow.
- const allocated = await localStoreAllocateTarget(localStore, queryToTarget(fromBundledQuery(query.bundledQuery)));
- const localStoreImpl = debugCast(localStore);
- return localStoreImpl.persistence.runTransaction('Save named query', 'readwrite', transaction => {
- const readTime = fromVersion(query.readTime);
- // Simply save the query itself if it is older than what the SDK already
- // has.
- if (allocated.snapshotVersion.compareTo(readTime) >= 0) {
- return localStoreImpl.bundleCache.saveNamedQuery(transaction, query);
- }
- // Update existing target data because the query from the bundle is newer.
- const newTargetData = allocated.withResumeToken(ByteString.EMPTY_BYTE_STRING, readTime);
- localStoreImpl.targetDataByTarget =
- localStoreImpl.targetDataByTarget.insert(newTargetData.targetId, newTargetData);
- return localStoreImpl.targetCache
- .updateTargetData(transaction, newTargetData)
- .next(() => localStoreImpl.targetCache.removeMatchingKeysForTargetId(transaction, allocated.targetId))
- .next(() => localStoreImpl.targetCache.addMatchingKeys(transaction, documents, allocated.targetId))
- .next(() => localStoreImpl.bundleCache.saveNamedQuery(transaction, query));
- });
- }
- async function localStoreConfigureFieldIndexes(localStore, newFieldIndexes) {
- const localStoreImpl = debugCast(localStore);
- const indexManager = localStoreImpl.indexManager;
- const promises = [];
- return localStoreImpl.persistence.runTransaction('Configure indexes', 'readwrite', transaction => indexManager
- .getFieldIndexes(transaction)
- .next(oldFieldIndexes => diffArrays(oldFieldIndexes, newFieldIndexes, fieldIndexSemanticComparator, fieldIndex => {
- promises.push(indexManager.addFieldIndex(transaction, fieldIndex));
- }, fieldIndex => {
- promises.push(indexManager.deleteFieldIndex(transaction, fieldIndex));
- }))
- .next(() => PersistencePromise.waitFor(promises)));
- }
- /**
- * @license
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * The Firestore query engine.
- *
- * Firestore queries can be executed in three modes. The Query Engine determines
- * what mode to use based on what data is persisted. The mode only determines
- * the runtime complexity of the query - the result set is equivalent across all
- * implementations.
- *
- * The Query engine will use indexed-based execution if a user has configured
- * any index that can be used to execute query (via `setIndexConfiguration()`).
- * Otherwise, the engine will try to optimize the query by re-using a previously
- * persisted query result. If that is not possible, the query will be executed
- * via a full collection scan.
- *
- * Index-based execution is the default when available. The query engine
- * supports partial indexed execution and merges the result from the index
- * lookup with documents that have not yet been indexed. The index evaluation
- * matches the backend's format and as such, the SDK can use indexing for all
- * queries that the backend supports.
- *
- * If no index exists, the query engine tries to take advantage of the target
- * document mapping in the TargetCache. These mappings exists for all queries
- * that have been synced with the backend at least once and allow the query
- * engine to only read documents that previously matched a query plus any
- * documents that were edited after the query was last listened to.
- *
- * There are some cases when this optimization is not guaranteed to produce
- * the same results as full collection scans. In these cases, query
- * processing falls back to full scans. These cases are:
- *
- * - Limit queries where a document that matched the query previously no longer
- * matches the query.
- *
- * - Limit queries where a document edit may cause the document to sort below
- * another document that is in the local cache.
- *
- * - Queries that have never been CURRENT or free of limbo documents.
- */
- class QueryEngine {
- constructor() {
- this.initialized = false;
- }
- /** Sets the document view to query against. */
- initialize(localDocuments, indexManager) {
- this.localDocumentsView = localDocuments;
- this.indexManager = indexManager;
- this.initialized = true;
- }
- /** Returns all local documents matching the specified query. */
- getDocumentsMatchingQuery(transaction, query, lastLimboFreeSnapshotVersion, remoteKeys) {
- return this.performQueryUsingIndex(transaction, query)
- .next(result => result
- ? result
- : this.performQueryUsingRemoteKeys(transaction, query, remoteKeys, lastLimboFreeSnapshotVersion))
- .next(result => result ? result : this.executeFullCollectionScan(transaction, query));
- }
- /**
- * Performs an indexed query that evaluates the query based on a collection's
- * persisted index values. Returns `null` if an index is not available.
- */
- performQueryUsingIndex(transaction, query) {
- if (queryMatchesAllDocuments(query)) {
- // Queries that match all documents don't benefit from using
- // key-based lookups. It is more efficient to scan all documents in a
- // collection, rather than to perform individual lookups.
- return PersistencePromise.resolve(null);
- }
- let target = queryToTarget(query);
- return this.indexManager
- .getIndexType(transaction, target)
- .next(indexType => {
- if (indexType === 0 /* IndexType.NONE */) {
- // The target cannot be served from any index.
- return null;
- }
- if (query.limit !== null && indexType === 1 /* IndexType.PARTIAL */) {
- // We cannot apply a limit for targets that are served using a partial
- // index. If a partial index will be used to serve the target, the
- // query may return a superset of documents that match the target
- // (e.g. if the index doesn't include all the target's filters), or
- // may return the correct set of documents in the wrong order (e.g. if
- // the index doesn't include a segment for one of the orderBys).
- // Therefore, a limit should not be applied in such cases.
- query = queryWithLimit(query, null, "F" /* LimitType.First */);
- target = queryToTarget(query);
- }
- return this.indexManager
- .getDocumentsMatchingTarget(transaction, target)
- .next(keys => {
- const sortedKeys = documentKeySet(...keys);
- return this.localDocumentsView
- .getDocuments(transaction, sortedKeys)
- .next(indexedDocuments => {
- return this.indexManager
- .getMinOffset(transaction, target)
- .next(offset => {
- const previousResults = this.applyQuery(query, indexedDocuments);
- if (this.needsRefill(query, previousResults, sortedKeys, offset.readTime)) {
- // A limit query whose boundaries change due to local
- // edits can be re-run against the cache by excluding the
- // limit. This ensures that all documents that match the
- // query's filters are included in the result set. The SDK
- // can then apply the limit once all local edits are
- // incorporated.
- return this.performQueryUsingIndex(transaction, queryWithLimit(query, null, "F" /* LimitType.First */));
- }
- return this.appendRemainingResults(transaction, previousResults, query, offset);
- });
- });
- });
- });
- }
- /**
- * Performs a query based on the target's persisted query mapping. Returns
- * `null` if the mapping is not available or cannot be used.
- */
- performQueryUsingRemoteKeys(transaction, query, remoteKeys, lastLimboFreeSnapshotVersion) {
- if (queryMatchesAllDocuments(query)) {
- // Queries that match all documents don't benefit from using
- // key-based lookups. It is more efficient to scan all documents in a
- // collection, rather than to perform individual lookups.
- return this.executeFullCollectionScan(transaction, query);
- }
- // Queries that have never seen a snapshot without limbo free documents
- // should also be run as a full collection scan.
- if (lastLimboFreeSnapshotVersion.isEqual(SnapshotVersion.min())) {
- return this.executeFullCollectionScan(transaction, query);
- }
- return this.localDocumentsView.getDocuments(transaction, remoteKeys).next(documents => {
- const previousResults = this.applyQuery(query, documents);
- if (this.needsRefill(query, previousResults, remoteKeys, lastLimboFreeSnapshotVersion)) {
- return this.executeFullCollectionScan(transaction, query);
- }
- if (getLogLevel() <= logger.LogLevel.DEBUG) {
- logDebug('QueryEngine', 'Re-using previous result from %s to execute query: %s', lastLimboFreeSnapshotVersion.toString(), stringifyQuery(query));
- }
- // Retrieve all results for documents that were updated since the last
- // limbo-document free remote snapshot.
- return this.appendRemainingResults(transaction, previousResults, query, newIndexOffsetSuccessorFromReadTime(lastLimboFreeSnapshotVersion, INITIAL_LARGEST_BATCH_ID));
- });
- }
- /** Applies the query filter and sorting to the provided documents. */
- applyQuery(query, documents) {
- // Sort the documents and re-apply the query filter since previously
- // matching documents do not necessarily still match the query.
- let queryResults = new SortedSet(newQueryComparator(query));
- documents.forEach((_, maybeDoc) => {
- if (queryMatches(query, maybeDoc)) {
- queryResults = queryResults.add(maybeDoc);
- }
- });
- return queryResults;
- }
- /**
- * Determines if a limit query needs to be refilled from cache, making it
- * ineligible for index-free execution.
- *
- * @param query - The query.
- * @param sortedPreviousResults - The documents that matched the query when it
- * was last synchronized, sorted by the query's comparator.
- * @param remoteKeys - The document keys that matched the query at the last
- * snapshot.
- * @param limboFreeSnapshotVersion - The version of the snapshot when the
- * query was last synchronized.
- */
- needsRefill(query, sortedPreviousResults, remoteKeys, limboFreeSnapshotVersion) {
- if (query.limit === null) {
- // Queries without limits do not need to be refilled.
- return false;
- }
- if (remoteKeys.size !== sortedPreviousResults.size) {
- // The query needs to be refilled if a previously matching document no
- // longer matches.
- return true;
- }
- // Limit queries are not eligible for index-free query execution if there is
- // a potential that an older document from cache now sorts before a document
- // that was previously part of the limit. This, however, can only happen if
- // the document at the edge of the limit goes out of limit.
- // If a document that is not the limit boundary sorts differently,
- // the boundary of the limit itself did not change and documents from cache
- // will continue to be "rejected" by this boundary. Therefore, we can ignore
- // any modifications that don't affect the last document.
- const docAtLimitEdge = query.limitType === "F" /* LimitType.First */
- ? sortedPreviousResults.last()
- : sortedPreviousResults.first();
- if (!docAtLimitEdge) {
- // We don't need to refill the query if there were already no documents.
- return false;
- }
- return (docAtLimitEdge.hasPendingWrites ||
- docAtLimitEdge.version.compareTo(limboFreeSnapshotVersion) > 0);
- }
- executeFullCollectionScan(transaction, query) {
- if (getLogLevel() <= logger.LogLevel.DEBUG) {
- logDebug('QueryEngine', 'Using full collection scan to execute query:', stringifyQuery(query));
- }
- return this.localDocumentsView.getDocumentsMatchingQuery(transaction, query, IndexOffset.min());
- }
- /**
- * Combines the results from an indexed execution with the remaining documents
- * that have not yet been indexed.
- */
- appendRemainingResults(transaction, indexedResults, query, offset) {
- // Retrieve all results for documents that were updated since the offset.
- return this.localDocumentsView
- .getDocumentsMatchingQuery(transaction, query, offset)
- .next(remainingResults => {
- // Merge with existing results
- indexedResults.forEach(d => {
- remainingResults = remainingResults.insert(d.key, d);
- });
- return remainingResults;
- });
- }
- }
- /**
- * @license
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- // The format of the LocalStorage key that stores the client state is:
- // firestore_clients_<persistence_prefix>_<instance_key>
- const CLIENT_STATE_KEY_PREFIX = 'firestore_clients';
- /** Assembles the key for a client state in WebStorage */
- function createWebStorageClientStateKey(persistenceKey, clientId) {
- return `${CLIENT_STATE_KEY_PREFIX}_${persistenceKey}_${clientId}`;
- }
- // The format of the WebStorage key that stores the mutation state is:
- // firestore_mutations_<persistence_prefix>_<batch_id>
- // (for unauthenticated users)
- // or: firestore_mutations_<persistence_prefix>_<batch_id>_<user_uid>
- //
- // 'user_uid' is last to avoid needing to escape '_' characters that it might
- // contain.
- const MUTATION_BATCH_KEY_PREFIX = 'firestore_mutations';
- /** Assembles the key for a mutation batch in WebStorage */
- function createWebStorageMutationBatchKey(persistenceKey, user, batchId) {
- let mutationKey = `${MUTATION_BATCH_KEY_PREFIX}_${persistenceKey}_${batchId}`;
- if (user.isAuthenticated()) {
- mutationKey += `_${user.uid}`;
- }
- return mutationKey;
- }
- // The format of the WebStorage key that stores a query target's metadata is:
- // firestore_targets_<persistence_prefix>_<target_id>
- const QUERY_TARGET_KEY_PREFIX = 'firestore_targets';
- /** Assembles the key for a query state in WebStorage */
- function createWebStorageQueryTargetMetadataKey(persistenceKey, targetId) {
- return `${QUERY_TARGET_KEY_PREFIX}_${persistenceKey}_${targetId}`;
- }
- // The WebStorage prefix that stores the primary tab's online state. The
- // format of the key is:
- // firestore_online_state_<persistence_prefix>
- const ONLINE_STATE_KEY_PREFIX = 'firestore_online_state';
- /** Assembles the key for the online state of the primary tab. */
- function createWebStorageOnlineStateKey(persistenceKey) {
- return `${ONLINE_STATE_KEY_PREFIX}_${persistenceKey}`;
- }
- // The WebStorage prefix that plays as a event to indicate the remote documents
- // might have changed due to some secondary tabs loading a bundle.
- // format of the key is:
- // firestore_bundle_loaded_v2_<persistenceKey>
- // The version ending with "v2" stores the list of modified collection groups.
- const BUNDLE_LOADED_KEY_PREFIX = 'firestore_bundle_loaded_v2';
- function createBundleLoadedKey(persistenceKey) {
- return `${BUNDLE_LOADED_KEY_PREFIX}_${persistenceKey}`;
- }
- // The WebStorage key prefix for the key that stores the last sequence number allocated. The key
- // looks like 'firestore_sequence_number_<persistence_prefix>'.
- const SEQUENCE_NUMBER_KEY_PREFIX = 'firestore_sequence_number';
- /** Assembles the key for the current sequence number. */
- function createWebStorageSequenceNumberKey(persistenceKey) {
- return `${SEQUENCE_NUMBER_KEY_PREFIX}_${persistenceKey}`;
- }
- /**
- * @license
- * Copyright 2018 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const LOG_TAG$a = 'SharedClientState';
- /**
- * Holds the state of a mutation batch, including its user ID, batch ID and
- * whether the batch is 'pending', 'acknowledged' or 'rejected'.
- */
- // Visible for testing
- class MutationMetadata {
- constructor(user, batchId, state, error) {
- this.user = user;
- this.batchId = batchId;
- this.state = state;
- this.error = error;
- }
- /**
- * Parses a MutationMetadata from its JSON representation in WebStorage.
- * Logs a warning and returns null if the format of the data is not valid.
- */
- static fromWebStorageEntry(user, batchId, value) {
- const mutationBatch = JSON.parse(value);
- let validData = typeof mutationBatch === 'object' &&
- ['pending', 'acknowledged', 'rejected'].indexOf(mutationBatch.state) !==
- -1 &&
- (mutationBatch.error === undefined ||
- typeof mutationBatch.error === 'object');
- let firestoreError = undefined;
- if (validData && mutationBatch.error) {
- validData =
- typeof mutationBatch.error.message === 'string' &&
- typeof mutationBatch.error.code === 'string';
- if (validData) {
- firestoreError = new FirestoreError(mutationBatch.error.code, mutationBatch.error.message);
- }
- }
- if (validData) {
- return new MutationMetadata(user, batchId, mutationBatch.state, firestoreError);
- }
- else {
- logError(LOG_TAG$a, `Failed to parse mutation state for ID '${batchId}': ${value}`);
- return null;
- }
- }
- toWebStorageJSON() {
- const batchMetadata = {
- state: this.state,
- updateTimeMs: Date.now() // Modify the existing value to trigger update.
- };
- if (this.error) {
- batchMetadata.error = {
- code: this.error.code,
- message: this.error.message
- };
- }
- return JSON.stringify(batchMetadata);
- }
- }
- /**
- * Holds the state of a query target, including its target ID and whether the
- * target is 'not-current', 'current' or 'rejected'.
- */
- // Visible for testing
- class QueryTargetMetadata {
- constructor(targetId, state, error) {
- this.targetId = targetId;
- this.state = state;
- this.error = error;
- }
- /**
- * Parses a QueryTargetMetadata from its JSON representation in WebStorage.
- * Logs a warning and returns null if the format of the data is not valid.
- */
- static fromWebStorageEntry(targetId, value) {
- const targetState = JSON.parse(value);
- let validData = typeof targetState === 'object' &&
- ['not-current', 'current', 'rejected'].indexOf(targetState.state) !==
- -1 &&
- (targetState.error === undefined ||
- typeof targetState.error === 'object');
- let firestoreError = undefined;
- if (validData && targetState.error) {
- validData =
- typeof targetState.error.message === 'string' &&
- typeof targetState.error.code === 'string';
- if (validData) {
- firestoreError = new FirestoreError(targetState.error.code, targetState.error.message);
- }
- }
- if (validData) {
- return new QueryTargetMetadata(targetId, targetState.state, firestoreError);
- }
- else {
- logError(LOG_TAG$a, `Failed to parse target state for ID '${targetId}': ${value}`);
- return null;
- }
- }
- toWebStorageJSON() {
- const targetState = {
- state: this.state,
- updateTimeMs: Date.now() // Modify the existing value to trigger update.
- };
- if (this.error) {
- targetState.error = {
- code: this.error.code,
- message: this.error.message
- };
- }
- return JSON.stringify(targetState);
- }
- }
- /**
- * This class represents the immutable ClientState for a client read from
- * WebStorage, containing the list of active query targets.
- */
- class RemoteClientState {
- constructor(clientId, activeTargetIds) {
- this.clientId = clientId;
- this.activeTargetIds = activeTargetIds;
- }
- /**
- * Parses a RemoteClientState from the JSON representation in WebStorage.
- * Logs a warning and returns null if the format of the data is not valid.
- */
- static fromWebStorageEntry(clientId, value) {
- const clientState = JSON.parse(value);
- let validData = typeof clientState === 'object' &&
- clientState.activeTargetIds instanceof Array;
- let activeTargetIdsSet = targetIdSet();
- for (let i = 0; validData && i < clientState.activeTargetIds.length; ++i) {
- validData = isSafeInteger(clientState.activeTargetIds[i]);
- activeTargetIdsSet = activeTargetIdsSet.add(clientState.activeTargetIds[i]);
- }
- if (validData) {
- return new RemoteClientState(clientId, activeTargetIdsSet);
- }
- else {
- logError(LOG_TAG$a, `Failed to parse client data for instance '${clientId}': ${value}`);
- return null;
- }
- }
- }
- /**
- * This class represents the online state for all clients participating in
- * multi-tab. The online state is only written to by the primary client, and
- * used in secondary clients to update their query views.
- */
- class SharedOnlineState {
- constructor(clientId, onlineState) {
- this.clientId = clientId;
- this.onlineState = onlineState;
- }
- /**
- * Parses a SharedOnlineState from its JSON representation in WebStorage.
- * Logs a warning and returns null if the format of the data is not valid.
- */
- static fromWebStorageEntry(value) {
- const onlineState = JSON.parse(value);
- const validData = typeof onlineState === 'object' &&
- ['Unknown', 'Online', 'Offline'].indexOf(onlineState.onlineState) !==
- -1 &&
- typeof onlineState.clientId === 'string';
- if (validData) {
- return new SharedOnlineState(onlineState.clientId, onlineState.onlineState);
- }
- else {
- logError(LOG_TAG$a, `Failed to parse online state: ${value}`);
- return null;
- }
- }
- }
- /**
- * Metadata state of the local client. Unlike `RemoteClientState`, this class is
- * mutable and keeps track of all pending mutations, which allows us to
- * update the range of pending mutation batch IDs as new mutations are added or
- * removed.
- *
- * The data in `LocalClientState` is not read from WebStorage and instead
- * updated via its instance methods. The updated state can be serialized via
- * `toWebStorageJSON()`.
- */
- // Visible for testing.
- class LocalClientState {
- constructor() {
- this.activeTargetIds = targetIdSet();
- }
- addQueryTarget(targetId) {
- this.activeTargetIds = this.activeTargetIds.add(targetId);
- }
- removeQueryTarget(targetId) {
- this.activeTargetIds = this.activeTargetIds.delete(targetId);
- }
- /**
- * Converts this entry into a JSON-encoded format we can use for WebStorage.
- * Does not encode `clientId` as it is part of the key in WebStorage.
- */
- toWebStorageJSON() {
- const data = {
- activeTargetIds: this.activeTargetIds.toArray(),
- updateTimeMs: Date.now() // Modify the existing value to trigger update.
- };
- return JSON.stringify(data);
- }
- }
- /**
- * `WebStorageSharedClientState` uses WebStorage (window.localStorage) as the
- * backing store for the SharedClientState. It keeps track of all active
- * clients and supports modifications of the local client's data.
- */
- class WebStorageSharedClientState {
- constructor(window, queue, persistenceKey, localClientId, initialUser) {
- this.window = window;
- this.queue = queue;
- this.persistenceKey = persistenceKey;
- this.localClientId = localClientId;
- this.syncEngine = null;
- this.onlineStateHandler = null;
- this.sequenceNumberHandler = null;
- this.storageListener = this.handleWebStorageEvent.bind(this);
- this.activeClients = new SortedMap(primitiveComparator);
- this.started = false;
- /**
- * Captures WebStorage events that occur before `start()` is called. These
- * events are replayed once `WebStorageSharedClientState` is started.
- */
- this.earlyEvents = [];
- // Escape the special characters mentioned here:
- // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
- const escapedPersistenceKey = persistenceKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- this.storage = this.window.localStorage;
- this.currentUser = initialUser;
- this.localClientStorageKey = createWebStorageClientStateKey(this.persistenceKey, this.localClientId);
- this.sequenceNumberKey = createWebStorageSequenceNumberKey(this.persistenceKey);
- this.activeClients = this.activeClients.insert(this.localClientId, new LocalClientState());
- this.clientStateKeyRe = new RegExp(`^${CLIENT_STATE_KEY_PREFIX}_${escapedPersistenceKey}_([^_]*)$`);
- this.mutationBatchKeyRe = new RegExp(`^${MUTATION_BATCH_KEY_PREFIX}_${escapedPersistenceKey}_(\\d+)(?:_(.*))?$`);
- this.queryTargetKeyRe = new RegExp(`^${QUERY_TARGET_KEY_PREFIX}_${escapedPersistenceKey}_(\\d+)$`);
- this.onlineStateKey = createWebStorageOnlineStateKey(this.persistenceKey);
- this.bundleLoadedKey = createBundleLoadedKey(this.persistenceKey);
- // Rather than adding the storage observer during start(), we add the
- // storage observer during initialization. This ensures that we collect
- // events before other components populate their initial state (during their
- // respective start() calls). Otherwise, we might for example miss a
- // mutation that is added after LocalStore's start() processed the existing
- // mutations but before we observe WebStorage events.
- this.window.addEventListener('storage', this.storageListener);
- }
- /** Returns 'true' if WebStorage is available in the current environment. */
- static isAvailable(window) {
- return !!(window && window.localStorage);
- }
- async start() {
- // Retrieve the list of existing clients to backfill the data in
- // SharedClientState.
- const existingClients = await this.syncEngine.getActiveClients();
- for (const clientId of existingClients) {
- if (clientId === this.localClientId) {
- continue;
- }
- const storageItem = this.getItem(createWebStorageClientStateKey(this.persistenceKey, clientId));
- if (storageItem) {
- const clientState = RemoteClientState.fromWebStorageEntry(clientId, storageItem);
- if (clientState) {
- this.activeClients = this.activeClients.insert(clientState.clientId, clientState);
- }
- }
- }
- this.persistClientState();
- // Check if there is an existing online state and call the callback handler
- // if applicable.
- const onlineStateJSON = this.storage.getItem(this.onlineStateKey);
- if (onlineStateJSON) {
- const onlineState = this.fromWebStorageOnlineState(onlineStateJSON);
- if (onlineState) {
- this.handleOnlineStateEvent(onlineState);
- }
- }
- for (const event of this.earlyEvents) {
- this.handleWebStorageEvent(event);
- }
- this.earlyEvents = [];
- // Register a window unload hook to remove the client metadata entry from
- // WebStorage even if `shutdown()` was not called.
- this.window.addEventListener('pagehide', () => this.shutdown());
- this.started = true;
- }
- writeSequenceNumber(sequenceNumber) {
- this.setItem(this.sequenceNumberKey, JSON.stringify(sequenceNumber));
- }
- getAllActiveQueryTargets() {
- return this.extractActiveQueryTargets(this.activeClients);
- }
- isActiveQueryTarget(targetId) {
- let found = false;
- this.activeClients.forEach((key, value) => {
- if (value.activeTargetIds.has(targetId)) {
- found = true;
- }
- });
- return found;
- }
- addPendingMutation(batchId) {
- this.persistMutationState(batchId, 'pending');
- }
- updateMutationState(batchId, state, error) {
- this.persistMutationState(batchId, state, error);
- // Once a final mutation result is observed by other clients, they no longer
- // access the mutation's metadata entry. Since WebStorage replays events
- // in order, it is safe to delete the entry right after updating it.
- this.removeMutationState(batchId);
- }
- addLocalQueryTarget(targetId) {
- let queryState = 'not-current';
- // Lookup an existing query state if the target ID was already registered
- // by another tab
- if (this.isActiveQueryTarget(targetId)) {
- const storageItem = this.storage.getItem(createWebStorageQueryTargetMetadataKey(this.persistenceKey, targetId));
- if (storageItem) {
- const metadata = QueryTargetMetadata.fromWebStorageEntry(targetId, storageItem);
- if (metadata) {
- queryState = metadata.state;
- }
- }
- }
- this.localClientState.addQueryTarget(targetId);
- this.persistClientState();
- return queryState;
- }
- removeLocalQueryTarget(targetId) {
- this.localClientState.removeQueryTarget(targetId);
- this.persistClientState();
- }
- isLocalQueryTarget(targetId) {
- return this.localClientState.activeTargetIds.has(targetId);
- }
- clearQueryState(targetId) {
- this.removeItem(createWebStorageQueryTargetMetadataKey(this.persistenceKey, targetId));
- }
- updateQueryState(targetId, state, error) {
- this.persistQueryTargetState(targetId, state, error);
- }
- handleUserChange(user, removedBatchIds, addedBatchIds) {
- removedBatchIds.forEach(batchId => {
- this.removeMutationState(batchId);
- });
- this.currentUser = user;
- addedBatchIds.forEach(batchId => {
- this.addPendingMutation(batchId);
- });
- }
- setOnlineState(onlineState) {
- this.persistOnlineState(onlineState);
- }
- notifyBundleLoaded(collectionGroups) {
- this.persistBundleLoadedState(collectionGroups);
- }
- shutdown() {
- if (this.started) {
- this.window.removeEventListener('storage', this.storageListener);
- this.removeItem(this.localClientStorageKey);
- this.started = false;
- }
- }
- getItem(key) {
- const value = this.storage.getItem(key);
- logDebug(LOG_TAG$a, 'READ', key, value);
- return value;
- }
- setItem(key, value) {
- logDebug(LOG_TAG$a, 'SET', key, value);
- this.storage.setItem(key, value);
- }
- removeItem(key) {
- logDebug(LOG_TAG$a, 'REMOVE', key);
- this.storage.removeItem(key);
- }
- handleWebStorageEvent(event) {
- // Note: The function is typed to take Event to be interface-compatible with
- // `Window.addEventListener`.
- const storageEvent = event;
- if (storageEvent.storageArea === this.storage) {
- logDebug(LOG_TAG$a, 'EVENT', storageEvent.key, storageEvent.newValue);
- if (storageEvent.key === this.localClientStorageKey) {
- logError('Received WebStorage notification for local change. Another client might have ' +
- 'garbage-collected our state');
- return;
- }
- this.queue.enqueueRetryable(async () => {
- if (!this.started) {
- this.earlyEvents.push(storageEvent);
- return;
- }
- if (storageEvent.key === null) {
- return;
- }
- if (this.clientStateKeyRe.test(storageEvent.key)) {
- if (storageEvent.newValue != null) {
- const clientState = this.fromWebStorageClientState(storageEvent.key, storageEvent.newValue);
- if (clientState) {
- return this.handleClientStateEvent(clientState.clientId, clientState);
- }
- }
- else {
- const clientId = this.fromWebStorageClientStateKey(storageEvent.key);
- return this.handleClientStateEvent(clientId, null);
- }
- }
- else if (this.mutationBatchKeyRe.test(storageEvent.key)) {
- if (storageEvent.newValue !== null) {
- const mutationMetadata = this.fromWebStorageMutationMetadata(storageEvent.key, storageEvent.newValue);
- if (mutationMetadata) {
- return this.handleMutationBatchEvent(mutationMetadata);
- }
- }
- }
- else if (this.queryTargetKeyRe.test(storageEvent.key)) {
- if (storageEvent.newValue !== null) {
- const queryTargetMetadata = this.fromWebStorageQueryTargetMetadata(storageEvent.key, storageEvent.newValue);
- if (queryTargetMetadata) {
- return this.handleQueryTargetEvent(queryTargetMetadata);
- }
- }
- }
- else if (storageEvent.key === this.onlineStateKey) {
- if (storageEvent.newValue !== null) {
- const onlineState = this.fromWebStorageOnlineState(storageEvent.newValue);
- if (onlineState) {
- return this.handleOnlineStateEvent(onlineState);
- }
- }
- }
- else if (storageEvent.key === this.sequenceNumberKey) {
- const sequenceNumber = fromWebStorageSequenceNumber(storageEvent.newValue);
- if (sequenceNumber !== ListenSequence.INVALID) {
- this.sequenceNumberHandler(sequenceNumber);
- }
- }
- else if (storageEvent.key === this.bundleLoadedKey) {
- const collectionGroups = this.fromWebStoreBundleLoadedState(storageEvent.newValue);
- await Promise.all(collectionGroups.map(cg => this.syncEngine.synchronizeWithChangedDocuments(cg)));
- }
- });
- }
- }
- get localClientState() {
- return this.activeClients.get(this.localClientId);
- }
- persistClientState() {
- this.setItem(this.localClientStorageKey, this.localClientState.toWebStorageJSON());
- }
- persistMutationState(batchId, state, error) {
- const mutationState = new MutationMetadata(this.currentUser, batchId, state, error);
- const mutationKey = createWebStorageMutationBatchKey(this.persistenceKey, this.currentUser, batchId);
- this.setItem(mutationKey, mutationState.toWebStorageJSON());
- }
- removeMutationState(batchId) {
- const mutationKey = createWebStorageMutationBatchKey(this.persistenceKey, this.currentUser, batchId);
- this.removeItem(mutationKey);
- }
- persistOnlineState(onlineState) {
- const entry = {
- clientId: this.localClientId,
- onlineState
- };
- this.storage.setItem(this.onlineStateKey, JSON.stringify(entry));
- }
- persistQueryTargetState(targetId, state, error) {
- const targetKey = createWebStorageQueryTargetMetadataKey(this.persistenceKey, targetId);
- const targetMetadata = new QueryTargetMetadata(targetId, state, error);
- this.setItem(targetKey, targetMetadata.toWebStorageJSON());
- }
- persistBundleLoadedState(collectionGroups) {
- const json = JSON.stringify(Array.from(collectionGroups));
- this.setItem(this.bundleLoadedKey, json);
- }
- /**
- * Parses a client state key in WebStorage. Returns null if the key does not
- * match the expected key format.
- */
- fromWebStorageClientStateKey(key) {
- const match = this.clientStateKeyRe.exec(key);
- return match ? match[1] : null;
- }
- /**
- * Parses a client state in WebStorage. Returns 'null' if the value could not
- * be parsed.
- */
- fromWebStorageClientState(key, value) {
- const clientId = this.fromWebStorageClientStateKey(key);
- return RemoteClientState.fromWebStorageEntry(clientId, value);
- }
- /**
- * Parses a mutation batch state in WebStorage. Returns 'null' if the value
- * could not be parsed.
- */
- fromWebStorageMutationMetadata(key, value) {
- const match = this.mutationBatchKeyRe.exec(key);
- const batchId = Number(match[1]);
- const userId = match[2] !== undefined ? match[2] : null;
- return MutationMetadata.fromWebStorageEntry(new User(userId), batchId, value);
- }
- /**
- * Parses a query target state from WebStorage. Returns 'null' if the value
- * could not be parsed.
- */
- fromWebStorageQueryTargetMetadata(key, value) {
- const match = this.queryTargetKeyRe.exec(key);
- const targetId = Number(match[1]);
- return QueryTargetMetadata.fromWebStorageEntry(targetId, value);
- }
- /**
- * Parses an online state from WebStorage. Returns 'null' if the value
- * could not be parsed.
- */
- fromWebStorageOnlineState(value) {
- return SharedOnlineState.fromWebStorageEntry(value);
- }
- fromWebStoreBundleLoadedState(value) {
- return JSON.parse(value);
- }
- async handleMutationBatchEvent(mutationBatch) {
- if (mutationBatch.user.uid !== this.currentUser.uid) {
- logDebug(LOG_TAG$a, `Ignoring mutation for non-active user ${mutationBatch.user.uid}`);
- return;
- }
- return this.syncEngine.applyBatchState(mutationBatch.batchId, mutationBatch.state, mutationBatch.error);
- }
- handleQueryTargetEvent(targetMetadata) {
- return this.syncEngine.applyTargetState(targetMetadata.targetId, targetMetadata.state, targetMetadata.error);
- }
- handleClientStateEvent(clientId, clientState) {
- const updatedClients = clientState
- ? this.activeClients.insert(clientId, clientState)
- : this.activeClients.remove(clientId);
- const existingTargets = this.extractActiveQueryTargets(this.activeClients);
- const newTargets = this.extractActiveQueryTargets(updatedClients);
- const addedTargets = [];
- const removedTargets = [];
- newTargets.forEach(targetId => {
- if (!existingTargets.has(targetId)) {
- addedTargets.push(targetId);
- }
- });
- existingTargets.forEach(targetId => {
- if (!newTargets.has(targetId)) {
- removedTargets.push(targetId);
- }
- });
- return this.syncEngine.applyActiveTargetsChange(addedTargets, removedTargets).then(() => {
- this.activeClients = updatedClients;
- });
- }
- handleOnlineStateEvent(onlineState) {
- // We check whether the client that wrote this online state is still active
- // by comparing its client ID to the list of clients kept active in
- // IndexedDb. If a client does not update their IndexedDb client state
- // within 5 seconds, it is considered inactive and we don't emit an online
- // state event.
- if (this.activeClients.get(onlineState.clientId)) {
- this.onlineStateHandler(onlineState.onlineState);
- }
- }
- extractActiveQueryTargets(clients) {
- let activeTargets = targetIdSet();
- clients.forEach((kev, value) => {
- activeTargets = activeTargets.unionWith(value.activeTargetIds);
- });
- return activeTargets;
- }
- }
- function fromWebStorageSequenceNumber(seqString) {
- let sequenceNumber = ListenSequence.INVALID;
- if (seqString != null) {
- try {
- const parsed = JSON.parse(seqString);
- hardAssert(typeof parsed === 'number');
- sequenceNumber = parsed;
- }
- catch (e) {
- logError(LOG_TAG$a, 'Failed to read sequence number from WebStorage', e);
- }
- }
- return sequenceNumber;
- }
- /**
- * `MemorySharedClientState` is a simple implementation of SharedClientState for
- * clients using memory persistence. The state in this class remains fully
- * isolated and no synchronization is performed.
- */
- class MemorySharedClientState {
- constructor() {
- this.localState = new LocalClientState();
- this.queryState = {};
- this.onlineStateHandler = null;
- this.sequenceNumberHandler = null;
- }
- addPendingMutation(batchId) {
- // No op.
- }
- updateMutationState(batchId, state, error) {
- // No op.
- }
- addLocalQueryTarget(targetId) {
- this.localState.addQueryTarget(targetId);
- return this.queryState[targetId] || 'not-current';
- }
- updateQueryState(targetId, state, error) {
- this.queryState[targetId] = state;
- }
- removeLocalQueryTarget(targetId) {
- this.localState.removeQueryTarget(targetId);
- }
- isLocalQueryTarget(targetId) {
- return this.localState.activeTargetIds.has(targetId);
- }
- clearQueryState(targetId) {
- delete this.queryState[targetId];
- }
- getAllActiveQueryTargets() {
- return this.localState.activeTargetIds;
- }
- isActiveQueryTarget(targetId) {
- return this.localState.activeTargetIds.has(targetId);
- }
- start() {
- this.localState = new LocalClientState();
- return Promise.resolve();
- }
- handleUserChange(user, removedBatchIds, addedBatchIds) {
- // No op.
- }
- setOnlineState(onlineState) {
- // No op.
- }
- shutdown() { }
- writeSequenceNumber(sequenceNumber) { }
- notifyBundleLoaded(collectionGroups) {
- // No op.
- }
- }
- /**
- * @license
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- class NoopConnectivityMonitor {
- addCallback(callback) {
- // No-op.
- }
- shutdown() {
- // No-op.
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Provides a simple helper class that implements the Stream interface to
- * bridge to other implementations that are streams but do not implement the
- * interface. The stream callbacks are invoked with the callOn... methods.
- */
- class StreamBridge {
- constructor(args) {
- this.sendFn = args.sendFn;
- this.closeFn = args.closeFn;
- }
- onOpen(callback) {
- this.wrappedOnOpen = callback;
- }
- onClose(callback) {
- this.wrappedOnClose = callback;
- }
- onMessage(callback) {
- this.wrappedOnMessage = callback;
- }
- close() {
- this.closeFn();
- }
- send(msg) {
- this.sendFn(msg);
- }
- callOnOpen() {
- this.wrappedOnOpen();
- }
- callOnClose(err) {
- this.wrappedOnClose(err);
- }
- callOnMessage(msg) {
- this.wrappedOnMessage(msg);
- }
- }
- /**
- * @license
- * Copyright 2023 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * The value returned from the most recent invocation of
- * `generateUniqueDebugId()`, or null if it has never been invoked.
- */
- let lastUniqueDebugId = null;
- /**
- * Generates and returns an initial value for `lastUniqueDebugId`.
- *
- * The returned value is randomly selected from a range of integers that are
- * represented as 8 hexadecimal digits. This means that (within reason) any
- * numbers generated by incrementing the returned number by 1 will also be
- * represented by 8 hexadecimal digits. This leads to all "IDs" having the same
- * length when converted to a hexadecimal string, making reading logs containing
- * these IDs easier to follow. And since the return value is randomly selected
- * it will help to differentiate between logs from different executions.
- */
- function generateInitialUniqueDebugId() {
- const minResult = 0x10000000;
- const maxResult = 0x90000000;
- const resultRange = maxResult - minResult;
- const resultOffset = Math.round(resultRange * Math.random());
- return minResult + resultOffset;
- }
- /**
- * Generates and returns a unique ID as a hexadecimal string.
- *
- * The returned ID is intended to be used in debug logging messages to help
- * correlate log messages that may be spatially separated in the logs, but
- * logically related. For example, a network connection could include the same
- * "debug ID" string in all of its log messages to help trace a specific
- * connection over time.
- *
- * @return the 10-character generated ID (e.g. "0xa1b2c3d4").
- */
- function generateUniqueDebugId() {
- if (lastUniqueDebugId === null) {
- lastUniqueDebugId = generateInitialUniqueDebugId();
- }
- else {
- lastUniqueDebugId++;
- }
- return '0x' + lastUniqueDebugId.toString(16);
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /*
- * Utilities for dealing with node.js-style APIs. See nodePromise for more
- * details.
- */
- /**
- * Creates a node-style callback that resolves or rejects a new Promise. The
- * callback is passed to the given action which can then use the callback as
- * a parameter to a node-style function.
- *
- * The intent is to directly bridge a node-style function (which takes a
- * callback) into a Promise without manually converting between the node-style
- * callback and the promise at each call.
- *
- * In effect it allows you to convert:
- *
- * @example
- * new Promise((resolve: (value?: fs.Stats) => void,
- * reject: (error?: any) => void) => {
- * fs.stat(path, (error?: any, stat?: fs.Stats) => {
- * if (error) {
- * reject(error);
- * } else {
- * resolve(stat);
- * }
- * });
- * });
- *
- * Into
- * @example
- * nodePromise((callback: NodeCallback<fs.Stats>) => {
- * fs.stat(path, callback);
- * });
- *
- * @param action - a function that takes a node-style callback as an argument
- * and then uses that callback to invoke some node-style API.
- * @returns a new Promise which will be rejected if the callback is given the
- * first Error parameter or will resolve to the value given otherwise.
- */
- function nodePromise(action) {
- return new Promise((resolve, reject) => {
- action((error, value) => {
- if (error) {
- reject(error);
- }
- else {
- resolve(value);
- }
- });
- });
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- // TODO: Fetch runtime version from grpc-js/package.json instead
- // when there's a cleaner way to dynamic require JSON in both Node ESM and CJS
- const grpcVersion = '1.7.3';
- const LOG_TAG$9 = 'GrpcConnection';
- const X_GOOG_API_CLIENT_VALUE = `gl-node/${process.versions.node} fire/${SDK_VERSION} grpc/${grpcVersion}`;
- function createMetadata(databasePath, authToken, appCheckToken, appId) {
- hardAssert(authToken === null || authToken.type === 'OAuth');
- const metadata = new grpc__namespace.Metadata();
- if (authToken) {
- authToken.headers.forEach((value, key) => metadata.set(key, value));
- }
- if (appCheckToken) {
- appCheckToken.headers.forEach((value, key) => metadata.set(key, value));
- }
- if (appId) {
- metadata.set('X-Firebase-GMPID', appId);
- }
- metadata.set('X-Goog-Api-Client', X_GOOG_API_CLIENT_VALUE);
- // These headers are used to improve routing and project isolation by the
- // backend.
- // TODO(b/199767712): We are keeping 'Google-Cloud-Resource-Prefix' until Emulators can be
- // released with cl/428820046. Currently blocked because Emulators are now built with Java
- // 11 from Google3.
- metadata.set('Google-Cloud-Resource-Prefix', databasePath);
- metadata.set('x-goog-request-params', databasePath);
- return metadata;
- }
- /**
- * A Connection implemented by GRPC-Node.
- */
- class GrpcConnection {
- constructor(protos, databaseInfo) {
- this.databaseInfo = databaseInfo;
- // We cache stubs for the most-recently-used token.
- this.cachedStub = null;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- this.firestore = protos['google']['firestore']['v1'];
- this.databasePath = `projects/${databaseInfo.databaseId.projectId}/databases/${databaseInfo.databaseId.database}`;
- }
- get shouldResourcePathBeIncludedInRequest() {
- // Both `invokeRPC()` and `invokeStreamingRPC()` ignore their `path` arguments, and expect
- // the "path" to be part of the given `request`.
- return true;
- }
- ensureActiveStub() {
- if (!this.cachedStub) {
- logDebug(LOG_TAG$9, 'Creating Firestore stub.');
- const credentials = this.databaseInfo.ssl
- ? grpc__namespace.credentials.createSsl()
- : grpc__namespace.credentials.createInsecure();
- this.cachedStub = new this.firestore.Firestore(this.databaseInfo.host, credentials);
- }
- return this.cachedStub;
- }
- invokeRPC(rpcName, path, request, authToken, appCheckToken) {
- const streamId = generateUniqueDebugId();
- const stub = this.ensureActiveStub();
- const metadata = createMetadata(this.databasePath, authToken, appCheckToken, this.databaseInfo.appId);
- const jsonRequest = Object.assign({ database: this.databasePath }, request);
- return nodePromise((callback) => {
- logDebug(LOG_TAG$9, `RPC '${rpcName}' ${streamId} invoked with request:`, request);
- return stub[rpcName](jsonRequest, metadata, (grpcError, value) => {
- if (grpcError) {
- logDebug(LOG_TAG$9, `RPC '${rpcName}' ${streamId} failed with error:`, grpcError);
- callback(new FirestoreError(mapCodeFromRpcCode(grpcError.code), grpcError.message));
- }
- else {
- logDebug(LOG_TAG$9, `RPC '${rpcName}' ${streamId} completed with response:`, value);
- callback(undefined, value);
- }
- });
- });
- }
- invokeStreamingRPC(rpcName, path, request, authToken, appCheckToken, expectedResponseCount) {
- const streamId = generateUniqueDebugId();
- const results = [];
- const responseDeferred = new Deferred();
- logDebug(LOG_TAG$9, `RPC '${rpcName}' ${streamId} invoked (streaming) with request:`, request);
- const stub = this.ensureActiveStub();
- const metadata = createMetadata(this.databasePath, authToken, appCheckToken, this.databaseInfo.appId);
- const jsonRequest = Object.assign(Object.assign({}, request), { database: this.databasePath });
- const stream = stub[rpcName](jsonRequest, metadata);
- let callbackFired = false;
- stream.on('data', (response) => {
- logDebug(LOG_TAG$9, `RPC ${rpcName} ${streamId} received result:`, response);
- results.push(response);
- if (expectedResponseCount !== undefined &&
- results.length === expectedResponseCount) {
- callbackFired = true;
- responseDeferred.resolve(results);
- }
- });
- stream.on('end', () => {
- logDebug(LOG_TAG$9, `RPC '${rpcName}' ${streamId} completed.`);
- if (!callbackFired) {
- callbackFired = true;
- responseDeferred.resolve(results);
- }
- });
- stream.on('error', (grpcError) => {
- logDebug(LOG_TAG$9, `RPC '${rpcName}' ${streamId} failed with error:`, grpcError);
- const code = mapCodeFromRpcCode(grpcError.code);
- responseDeferred.reject(new FirestoreError(code, grpcError.message));
- });
- return responseDeferred.promise;
- }
- // TODO(mikelehen): This "method" is a monster. Should be refactored.
- openStream(rpcName, authToken, appCheckToken) {
- const streamId = generateUniqueDebugId();
- const stub = this.ensureActiveStub();
- const metadata = createMetadata(this.databasePath, authToken, appCheckToken, this.databaseInfo.appId);
- const grpcStream = stub[rpcName](metadata);
- let closed = false;
- const close = (err) => {
- if (!closed) {
- closed = true;
- stream.callOnClose(err);
- grpcStream.end();
- }
- };
- const stream = new StreamBridge({
- sendFn: (msg) => {
- if (!closed) {
- logDebug(LOG_TAG$9, `RPC '${rpcName}' stream ${streamId} sending:`, msg);
- try {
- grpcStream.write(msg);
- }
- catch (e) {
- // This probably means we didn't conform to the proto. Make sure to
- // log the message we sent.
- logError('Failure sending:', msg);
- logError('Error:', e);
- throw e;
- }
- }
- else {
- logDebug(LOG_TAG$9, `RPC '${rpcName}' stream ${streamId} ` +
- 'not sending because gRPC stream is closed:', msg);
- }
- },
- closeFn: () => {
- logDebug(LOG_TAG$9, `RPC '${rpcName}' stream ${streamId} closed locally via close().`);
- close();
- }
- });
- grpcStream.on('data', (msg) => {
- if (!closed) {
- logDebug(LOG_TAG$9, `RPC '${rpcName}' stream ${streamId} received:`, msg);
- stream.callOnMessage(msg);
- }
- });
- grpcStream.on('end', () => {
- logDebug(LOG_TAG$9, `RPC '${rpcName}' stream ${streamId} ended.`);
- close();
- });
- grpcStream.on('error', (grpcError) => {
- if (!closed) {
- logWarn(LOG_TAG$9, `RPC '${rpcName}' stream ${streamId} error. Code:`, grpcError.code, 'Message:', grpcError.message);
- const code = mapCodeFromRpcCode(grpcError.code);
- close(new FirestoreError(code, grpcError.message));
- }
- });
- logDebug(LOG_TAG$9, `Opening RPC '${rpcName}' stream ${streamId} ` +
- `to ${this.databaseInfo.host}`);
- // TODO(dimond): Since grpc has no explicit open status (or does it?) we
- // simulate an onOpen in the next loop after the stream had it's listeners
- // registered
- setTimeout(() => {
- stream.callOnOpen();
- }, 0);
- return stream;
- }
- }
- const nested = {
- google: {
- nested: {
- protobuf: {
- options: {
- csharp_namespace: "Google.Protobuf.WellKnownTypes",
- go_package: "github.com/golang/protobuf/ptypes/wrappers",
- java_package: "com.google.protobuf",
- java_outer_classname: "WrappersProto",
- java_multiple_files: true,
- objc_class_prefix: "GPB",
- cc_enable_arenas: true,
- optimize_for: "SPEED"
- },
- nested: {
- Timestamp: {
- fields: {
- seconds: {
- type: "int64",
- id: 1
- },
- nanos: {
- type: "int32",
- id: 2
- }
- }
- },
- FileDescriptorSet: {
- fields: {
- file: {
- rule: "repeated",
- type: "FileDescriptorProto",
- id: 1
- }
- }
- },
- FileDescriptorProto: {
- fields: {
- name: {
- type: "string",
- id: 1
- },
- "package": {
- type: "string",
- id: 2
- },
- dependency: {
- rule: "repeated",
- type: "string",
- id: 3
- },
- publicDependency: {
- rule: "repeated",
- type: "int32",
- id: 10,
- options: {
- packed: false
- }
- },
- weakDependency: {
- rule: "repeated",
- type: "int32",
- id: 11,
- options: {
- packed: false
- }
- },
- messageType: {
- rule: "repeated",
- type: "DescriptorProto",
- id: 4
- },
- enumType: {
- rule: "repeated",
- type: "EnumDescriptorProto",
- id: 5
- },
- service: {
- rule: "repeated",
- type: "ServiceDescriptorProto",
- id: 6
- },
- extension: {
- rule: "repeated",
- type: "FieldDescriptorProto",
- id: 7
- },
- options: {
- type: "FileOptions",
- id: 8
- },
- sourceCodeInfo: {
- type: "SourceCodeInfo",
- id: 9
- },
- syntax: {
- type: "string",
- id: 12
- }
- }
- },
- DescriptorProto: {
- fields: {
- name: {
- type: "string",
- id: 1
- },
- field: {
- rule: "repeated",
- type: "FieldDescriptorProto",
- id: 2
- },
- extension: {
- rule: "repeated",
- type: "FieldDescriptorProto",
- id: 6
- },
- nestedType: {
- rule: "repeated",
- type: "DescriptorProto",
- id: 3
- },
- enumType: {
- rule: "repeated",
- type: "EnumDescriptorProto",
- id: 4
- },
- extensionRange: {
- rule: "repeated",
- type: "ExtensionRange",
- id: 5
- },
- oneofDecl: {
- rule: "repeated",
- type: "OneofDescriptorProto",
- id: 8
- },
- options: {
- type: "MessageOptions",
- id: 7
- },
- reservedRange: {
- rule: "repeated",
- type: "ReservedRange",
- id: 9
- },
- reservedName: {
- rule: "repeated",
- type: "string",
- id: 10
- }
- },
- nested: {
- ExtensionRange: {
- fields: {
- start: {
- type: "int32",
- id: 1
- },
- end: {
- type: "int32",
- id: 2
- }
- }
- },
- ReservedRange: {
- fields: {
- start: {
- type: "int32",
- id: 1
- },
- end: {
- type: "int32",
- id: 2
- }
- }
- }
- }
- },
- FieldDescriptorProto: {
- fields: {
- name: {
- type: "string",
- id: 1
- },
- number: {
- type: "int32",
- id: 3
- },
- label: {
- type: "Label",
- id: 4
- },
- type: {
- type: "Type",
- id: 5
- },
- typeName: {
- type: "string",
- id: 6
- },
- extendee: {
- type: "string",
- id: 2
- },
- defaultValue: {
- type: "string",
- id: 7
- },
- oneofIndex: {
- type: "int32",
- id: 9
- },
- jsonName: {
- type: "string",
- id: 10
- },
- options: {
- type: "FieldOptions",
- id: 8
- }
- },
- nested: {
- Type: {
- values: {
- TYPE_DOUBLE: 1,
- TYPE_FLOAT: 2,
- TYPE_INT64: 3,
- TYPE_UINT64: 4,
- TYPE_INT32: 5,
- TYPE_FIXED64: 6,
- TYPE_FIXED32: 7,
- TYPE_BOOL: 8,
- TYPE_STRING: 9,
- TYPE_GROUP: 10,
- TYPE_MESSAGE: 11,
- TYPE_BYTES: 12,
- TYPE_UINT32: 13,
- TYPE_ENUM: 14,
- TYPE_SFIXED32: 15,
- TYPE_SFIXED64: 16,
- TYPE_SINT32: 17,
- TYPE_SINT64: 18
- }
- },
- Label: {
- values: {
- LABEL_OPTIONAL: 1,
- LABEL_REQUIRED: 2,
- LABEL_REPEATED: 3
- }
- }
- }
- },
- OneofDescriptorProto: {
- fields: {
- name: {
- type: "string",
- id: 1
- },
- options: {
- type: "OneofOptions",
- id: 2
- }
- }
- },
- EnumDescriptorProto: {
- fields: {
- name: {
- type: "string",
- id: 1
- },
- value: {
- rule: "repeated",
- type: "EnumValueDescriptorProto",
- id: 2
- },
- options: {
- type: "EnumOptions",
- id: 3
- }
- }
- },
- EnumValueDescriptorProto: {
- fields: {
- name: {
- type: "string",
- id: 1
- },
- number: {
- type: "int32",
- id: 2
- },
- options: {
- type: "EnumValueOptions",
- id: 3
- }
- }
- },
- ServiceDescriptorProto: {
- fields: {
- name: {
- type: "string",
- id: 1
- },
- method: {
- rule: "repeated",
- type: "MethodDescriptorProto",
- id: 2
- },
- options: {
- type: "ServiceOptions",
- id: 3
- }
- }
- },
- MethodDescriptorProto: {
- fields: {
- name: {
- type: "string",
- id: 1
- },
- inputType: {
- type: "string",
- id: 2
- },
- outputType: {
- type: "string",
- id: 3
- },
- options: {
- type: "MethodOptions",
- id: 4
- },
- clientStreaming: {
- type: "bool",
- id: 5
- },
- serverStreaming: {
- type: "bool",
- id: 6
- }
- }
- },
- FileOptions: {
- fields: {
- javaPackage: {
- type: "string",
- id: 1
- },
- javaOuterClassname: {
- type: "string",
- id: 8
- },
- javaMultipleFiles: {
- type: "bool",
- id: 10
- },
- javaGenerateEqualsAndHash: {
- type: "bool",
- id: 20,
- options: {
- deprecated: true
- }
- },
- javaStringCheckUtf8: {
- type: "bool",
- id: 27
- },
- optimizeFor: {
- type: "OptimizeMode",
- id: 9,
- options: {
- "default": "SPEED"
- }
- },
- goPackage: {
- type: "string",
- id: 11
- },
- ccGenericServices: {
- type: "bool",
- id: 16
- },
- javaGenericServices: {
- type: "bool",
- id: 17
- },
- pyGenericServices: {
- type: "bool",
- id: 18
- },
- deprecated: {
- type: "bool",
- id: 23
- },
- ccEnableArenas: {
- type: "bool",
- id: 31
- },
- objcClassPrefix: {
- type: "string",
- id: 36
- },
- csharpNamespace: {
- type: "string",
- id: 37
- },
- uninterpretedOption: {
- rule: "repeated",
- type: "UninterpretedOption",
- id: 999
- }
- },
- extensions: [
- [
- 1000,
- 536870911
- ]
- ],
- reserved: [
- [
- 38,
- 38
- ]
- ],
- nested: {
- OptimizeMode: {
- values: {
- SPEED: 1,
- CODE_SIZE: 2,
- LITE_RUNTIME: 3
- }
- }
- }
- },
- MessageOptions: {
- fields: {
- messageSetWireFormat: {
- type: "bool",
- id: 1
- },
- noStandardDescriptorAccessor: {
- type: "bool",
- id: 2
- },
- deprecated: {
- type: "bool",
- id: 3
- },
- mapEntry: {
- type: "bool",
- id: 7
- },
- uninterpretedOption: {
- rule: "repeated",
- type: "UninterpretedOption",
- id: 999
- }
- },
- extensions: [
- [
- 1000,
- 536870911
- ]
- ],
- reserved: [
- [
- 8,
- 8
- ]
- ]
- },
- FieldOptions: {
- fields: {
- ctype: {
- type: "CType",
- id: 1,
- options: {
- "default": "STRING"
- }
- },
- packed: {
- type: "bool",
- id: 2
- },
- jstype: {
- type: "JSType",
- id: 6,
- options: {
- "default": "JS_NORMAL"
- }
- },
- lazy: {
- type: "bool",
- id: 5
- },
- deprecated: {
- type: "bool",
- id: 3
- },
- weak: {
- type: "bool",
- id: 10
- },
- uninterpretedOption: {
- rule: "repeated",
- type: "UninterpretedOption",
- id: 999
- }
- },
- extensions: [
- [
- 1000,
- 536870911
- ]
- ],
- reserved: [
- [
- 4,
- 4
- ]
- ],
- nested: {
- CType: {
- values: {
- STRING: 0,
- CORD: 1,
- STRING_PIECE: 2
- }
- },
- JSType: {
- values: {
- JS_NORMAL: 0,
- JS_STRING: 1,
- JS_NUMBER: 2
- }
- }
- }
- },
- OneofOptions: {
- fields: {
- uninterpretedOption: {
- rule: "repeated",
- type: "UninterpretedOption",
- id: 999
- }
- },
- extensions: [
- [
- 1000,
- 536870911
- ]
- ]
- },
- EnumOptions: {
- fields: {
- allowAlias: {
- type: "bool",
- id: 2
- },
- deprecated: {
- type: "bool",
- id: 3
- },
- uninterpretedOption: {
- rule: "repeated",
- type: "UninterpretedOption",
- id: 999
- }
- },
- extensions: [
- [
- 1000,
- 536870911
- ]
- ]
- },
- EnumValueOptions: {
- fields: {
- deprecated: {
- type: "bool",
- id: 1
- },
- uninterpretedOption: {
- rule: "repeated",
- type: "UninterpretedOption",
- id: 999
- }
- },
- extensions: [
- [
- 1000,
- 536870911
- ]
- ]
- },
- ServiceOptions: {
- fields: {
- deprecated: {
- type: "bool",
- id: 33
- },
- uninterpretedOption: {
- rule: "repeated",
- type: "UninterpretedOption",
- id: 999
- }
- },
- extensions: [
- [
- 1000,
- 536870911
- ]
- ]
- },
- MethodOptions: {
- fields: {
- deprecated: {
- type: "bool",
- id: 33
- },
- uninterpretedOption: {
- rule: "repeated",
- type: "UninterpretedOption",
- id: 999
- }
- },
- extensions: [
- [
- 1000,
- 536870911
- ]
- ]
- },
- UninterpretedOption: {
- fields: {
- name: {
- rule: "repeated",
- type: "NamePart",
- id: 2
- },
- identifierValue: {
- type: "string",
- id: 3
- },
- positiveIntValue: {
- type: "uint64",
- id: 4
- },
- negativeIntValue: {
- type: "int64",
- id: 5
- },
- doubleValue: {
- type: "double",
- id: 6
- },
- stringValue: {
- type: "bytes",
- id: 7
- },
- aggregateValue: {
- type: "string",
- id: 8
- }
- },
- nested: {
- NamePart: {
- fields: {
- namePart: {
- rule: "required",
- type: "string",
- id: 1
- },
- isExtension: {
- rule: "required",
- type: "bool",
- id: 2
- }
- }
- }
- }
- },
- SourceCodeInfo: {
- fields: {
- location: {
- rule: "repeated",
- type: "Location",
- id: 1
- }
- },
- nested: {
- Location: {
- fields: {
- path: {
- rule: "repeated",
- type: "int32",
- id: 1
- },
- span: {
- rule: "repeated",
- type: "int32",
- id: 2
- },
- leadingComments: {
- type: "string",
- id: 3
- },
- trailingComments: {
- type: "string",
- id: 4
- },
- leadingDetachedComments: {
- rule: "repeated",
- type: "string",
- id: 6
- }
- }
- }
- }
- },
- GeneratedCodeInfo: {
- fields: {
- annotation: {
- rule: "repeated",
- type: "Annotation",
- id: 1
- }
- },
- nested: {
- Annotation: {
- fields: {
- path: {
- rule: "repeated",
- type: "int32",
- id: 1
- },
- sourceFile: {
- type: "string",
- id: 2
- },
- begin: {
- type: "int32",
- id: 3
- },
- end: {
- type: "int32",
- id: 4
- }
- }
- }
- }
- },
- Struct: {
- fields: {
- fields: {
- keyType: "string",
- type: "Value",
- id: 1
- }
- }
- },
- Value: {
- oneofs: {
- kind: {
- oneof: [
- "nullValue",
- "numberValue",
- "stringValue",
- "boolValue",
- "structValue",
- "listValue"
- ]
- }
- },
- fields: {
- nullValue: {
- type: "NullValue",
- id: 1
- },
- numberValue: {
- type: "double",
- id: 2
- },
- stringValue: {
- type: "string",
- id: 3
- },
- boolValue: {
- type: "bool",
- id: 4
- },
- structValue: {
- type: "Struct",
- id: 5
- },
- listValue: {
- type: "ListValue",
- id: 6
- }
- }
- },
- NullValue: {
- values: {
- NULL_VALUE: 0
- }
- },
- ListValue: {
- fields: {
- values: {
- rule: "repeated",
- type: "Value",
- id: 1
- }
- }
- },
- Empty: {
- fields: {
- }
- },
- DoubleValue: {
- fields: {
- value: {
- type: "double",
- id: 1
- }
- }
- },
- FloatValue: {
- fields: {
- value: {
- type: "float",
- id: 1
- }
- }
- },
- Int64Value: {
- fields: {
- value: {
- type: "int64",
- id: 1
- }
- }
- },
- UInt64Value: {
- fields: {
- value: {
- type: "uint64",
- id: 1
- }
- }
- },
- Int32Value: {
- fields: {
- value: {
- type: "int32",
- id: 1
- }
- }
- },
- UInt32Value: {
- fields: {
- value: {
- type: "uint32",
- id: 1
- }
- }
- },
- BoolValue: {
- fields: {
- value: {
- type: "bool",
- id: 1
- }
- }
- },
- StringValue: {
- fields: {
- value: {
- type: "string",
- id: 1
- }
- }
- },
- BytesValue: {
- fields: {
- value: {
- type: "bytes",
- id: 1
- }
- }
- },
- Any: {
- fields: {
- typeUrl: {
- type: "string",
- id: 1
- },
- value: {
- type: "bytes",
- id: 2
- }
- }
- }
- }
- },
- firestore: {
- nested: {
- v1: {
- options: {
- csharp_namespace: "Google.Cloud.Firestore.V1",
- go_package: "google.golang.org/genproto/googleapis/firestore/v1;firestore",
- java_multiple_files: true,
- java_outer_classname: "WriteProto",
- java_package: "com.google.firestore.v1",
- objc_class_prefix: "GCFS",
- php_namespace: "Google\\Cloud\\Firestore\\V1",
- ruby_package: "Google::Cloud::Firestore::V1"
- },
- nested: {
- AggregationResult: {
- fields: {
- aggregateFields: {
- keyType: "string",
- type: "Value",
- id: 2
- }
- }
- },
- BitSequence: {
- fields: {
- bitmap: {
- type: "bytes",
- id: 1
- },
- padding: {
- type: "int32",
- id: 2
- }
- }
- },
- BloomFilter: {
- fields: {
- bits: {
- type: "BitSequence",
- id: 1
- },
- hashCount: {
- type: "int32",
- id: 2
- }
- }
- },
- DocumentMask: {
- fields: {
- fieldPaths: {
- rule: "repeated",
- type: "string",
- id: 1
- }
- }
- },
- Precondition: {
- oneofs: {
- conditionType: {
- oneof: [
- "exists",
- "updateTime"
- ]
- }
- },
- fields: {
- exists: {
- type: "bool",
- id: 1
- },
- updateTime: {
- type: "google.protobuf.Timestamp",
- id: 2
- }
- }
- },
- TransactionOptions: {
- oneofs: {
- mode: {
- oneof: [
- "readOnly",
- "readWrite"
- ]
- }
- },
- fields: {
- readOnly: {
- type: "ReadOnly",
- id: 2
- },
- readWrite: {
- type: "ReadWrite",
- id: 3
- }
- },
- nested: {
- ReadWrite: {
- fields: {
- retryTransaction: {
- type: "bytes",
- id: 1
- }
- }
- },
- ReadOnly: {
- oneofs: {
- consistencySelector: {
- oneof: [
- "readTime"
- ]
- }
- },
- fields: {
- readTime: {
- type: "google.protobuf.Timestamp",
- id: 2
- }
- }
- }
- }
- },
- Document: {
- fields: {
- name: {
- type: "string",
- id: 1
- },
- fields: {
- keyType: "string",
- type: "Value",
- id: 2
- },
- createTime: {
- type: "google.protobuf.Timestamp",
- id: 3
- },
- updateTime: {
- type: "google.protobuf.Timestamp",
- id: 4
- }
- }
- },
- Value: {
- oneofs: {
- valueType: {
- oneof: [
- "nullValue",
- "booleanValue",
- "integerValue",
- "doubleValue",
- "timestampValue",
- "stringValue",
- "bytesValue",
- "referenceValue",
- "geoPointValue",
- "arrayValue",
- "mapValue"
- ]
- }
- },
- fields: {
- nullValue: {
- type: "google.protobuf.NullValue",
- id: 11
- },
- booleanValue: {
- type: "bool",
- id: 1
- },
- integerValue: {
- type: "int64",
- id: 2
- },
- doubleValue: {
- type: "double",
- id: 3
- },
- timestampValue: {
- type: "google.protobuf.Timestamp",
- id: 10
- },
- stringValue: {
- type: "string",
- id: 17
- },
- bytesValue: {
- type: "bytes",
- id: 18
- },
- referenceValue: {
- type: "string",
- id: 5
- },
- geoPointValue: {
- type: "google.type.LatLng",
- id: 8
- },
- arrayValue: {
- type: "ArrayValue",
- id: 9
- },
- mapValue: {
- type: "MapValue",
- id: 6
- }
- }
- },
- ArrayValue: {
- fields: {
- values: {
- rule: "repeated",
- type: "Value",
- id: 1
- }
- }
- },
- MapValue: {
- fields: {
- fields: {
- keyType: "string",
- type: "Value",
- id: 1
- }
- }
- },
- Firestore: {
- options: {
- "(google.api.default_host)": "firestore.googleapis.com",
- "(google.api.oauth_scopes)": "https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/datastore"
- },
- methods: {
- GetDocument: {
- requestType: "GetDocumentRequest",
- responseType: "Document",
- options: {
- "(google.api.http).get": "/v1/{name=projects/*/databases/*/documents/*/**}"
- },
- parsedOptions: [
- {
- "(google.api.http)": {
- get: "/v1/{name=projects/*/databases/*/documents/*/**}"
- }
- }
- ]
- },
- ListDocuments: {
- requestType: "ListDocumentsRequest",
- responseType: "ListDocumentsResponse",
- options: {
- "(google.api.http).get": "/v1/{parent=projects/*/databases/*/documents/*/**}/{collection_id}"
- },
- parsedOptions: [
- {
- "(google.api.http)": {
- get: "/v1/{parent=projects/*/databases/*/documents/*/**}/{collection_id}"
- }
- }
- ]
- },
- UpdateDocument: {
- requestType: "UpdateDocumentRequest",
- responseType: "Document",
- options: {
- "(google.api.http).patch": "/v1/{document.name=projects/*/databases/*/documents/*/**}",
- "(google.api.http).body": "document",
- "(google.api.method_signature)": "document,update_mask"
- },
- parsedOptions: [
- {
- "(google.api.http)": {
- patch: "/v1/{document.name=projects/*/databases/*/documents/*/**}",
- body: "document"
- }
- },
- {
- "(google.api.method_signature)": "document,update_mask"
- }
- ]
- },
- DeleteDocument: {
- requestType: "DeleteDocumentRequest",
- responseType: "google.protobuf.Empty",
- options: {
- "(google.api.http).delete": "/v1/{name=projects/*/databases/*/documents/*/**}",
- "(google.api.method_signature)": "name"
- },
- parsedOptions: [
- {
- "(google.api.http)": {
- "delete": "/v1/{name=projects/*/databases/*/documents/*/**}"
- }
- },
- {
- "(google.api.method_signature)": "name"
- }
- ]
- },
- BatchGetDocuments: {
- requestType: "BatchGetDocumentsRequest",
- responseType: "BatchGetDocumentsResponse",
- responseStream: true,
- options: {
- "(google.api.http).post": "/v1/{database=projects/*/databases/*}/documents:batchGet",
- "(google.api.http).body": "*"
- },
- parsedOptions: [
- {
- "(google.api.http)": {
- post: "/v1/{database=projects/*/databases/*}/documents:batchGet",
- body: "*"
- }
- }
- ]
- },
- BeginTransaction: {
- requestType: "BeginTransactionRequest",
- responseType: "BeginTransactionResponse",
- options: {
- "(google.api.http).post": "/v1/{database=projects/*/databases/*}/documents:beginTransaction",
- "(google.api.http).body": "*",
- "(google.api.method_signature)": "database"
- },
- parsedOptions: [
- {
- "(google.api.http)": {
- post: "/v1/{database=projects/*/databases/*}/documents:beginTransaction",
- body: "*"
- }
- },
- {
- "(google.api.method_signature)": "database"
- }
- ]
- },
- Commit: {
- requestType: "CommitRequest",
- responseType: "CommitResponse",
- options: {
- "(google.api.http).post": "/v1/{database=projects/*/databases/*}/documents:commit",
- "(google.api.http).body": "*",
- "(google.api.method_signature)": "database,writes"
- },
- parsedOptions: [
- {
- "(google.api.http)": {
- post: "/v1/{database=projects/*/databases/*}/documents:commit",
- body: "*"
- }
- },
- {
- "(google.api.method_signature)": "database,writes"
- }
- ]
- },
- Rollback: {
- requestType: "RollbackRequest",
- responseType: "google.protobuf.Empty",
- options: {
- "(google.api.http).post": "/v1/{database=projects/*/databases/*}/documents:rollback",
- "(google.api.http).body": "*",
- "(google.api.method_signature)": "database,transaction"
- },
- parsedOptions: [
- {
- "(google.api.http)": {
- post: "/v1/{database=projects/*/databases/*}/documents:rollback",
- body: "*"
- }
- },
- {
- "(google.api.method_signature)": "database,transaction"
- }
- ]
- },
- RunQuery: {
- requestType: "RunQueryRequest",
- responseType: "RunQueryResponse",
- responseStream: true,
- options: {
- "(google.api.http).post": "/v1/{parent=projects/*/databases/*/documents}:runQuery",
- "(google.api.http).body": "*",
- "(google.api.http).additional_bindings.post": "/v1/{parent=projects/*/databases/*/documents/*/**}:runQuery",
- "(google.api.http).additional_bindings.body": "*"
- },
- parsedOptions: [
- {
- "(google.api.http)": {
- post: "/v1/{parent=projects/*/databases/*/documents}:runQuery",
- body: "*",
- additional_bindings: {
- post: "/v1/{parent=projects/*/databases/*/documents/*/**}:runQuery",
- body: "*"
- }
- }
- }
- ]
- },
- RunAggregationQuery: {
- requestType: "RunAggregationQueryRequest",
- responseType: "RunAggregationQueryResponse",
- responseStream: true,
- options: {
- "(google.api.http).post": "/v1/{parent=projects/*/databases/*/documents}:runAggregationQuery",
- "(google.api.http).body": "*",
- "(google.api.http).additional_bindings.post": "/v1/{parent=projects/*/databases/*/documents/*/**}:runAggregationQuery",
- "(google.api.http).additional_bindings.body": "*"
- },
- parsedOptions: [
- {
- "(google.api.http)": {
- post: "/v1/{parent=projects/*/databases/*/documents}:runAggregationQuery",
- body: "*",
- additional_bindings: {
- post: "/v1/{parent=projects/*/databases/*/documents/*/**}:runAggregationQuery",
- body: "*"
- }
- }
- }
- ]
- },
- PartitionQuery: {
- requestType: "PartitionQueryRequest",
- responseType: "PartitionQueryResponse",
- options: {
- "(google.api.http).post": "/v1/{parent=projects/*/databases/*/documents}:partitionQuery",
- "(google.api.http).body": "*",
- "(google.api.http).additional_bindings.post": "/v1/{parent=projects/*/databases/*/documents/*/**}:partitionQuery",
- "(google.api.http).additional_bindings.body": "*"
- },
- parsedOptions: [
- {
- "(google.api.http)": {
- post: "/v1/{parent=projects/*/databases/*/documents}:partitionQuery",
- body: "*",
- additional_bindings: {
- post: "/v1/{parent=projects/*/databases/*/documents/*/**}:partitionQuery",
- body: "*"
- }
- }
- }
- ]
- },
- Write: {
- requestType: "WriteRequest",
- requestStream: true,
- responseType: "WriteResponse",
- responseStream: true,
- options: {
- "(google.api.http).post": "/v1/{database=projects/*/databases/*}/documents:write",
- "(google.api.http).body": "*"
- },
- parsedOptions: [
- {
- "(google.api.http)": {
- post: "/v1/{database=projects/*/databases/*}/documents:write",
- body: "*"
- }
- }
- ]
- },
- Listen: {
- requestType: "ListenRequest",
- requestStream: true,
- responseType: "ListenResponse",
- responseStream: true,
- options: {
- "(google.api.http).post": "/v1/{database=projects/*/databases/*}/documents:listen",
- "(google.api.http).body": "*"
- },
- parsedOptions: [
- {
- "(google.api.http)": {
- post: "/v1/{database=projects/*/databases/*}/documents:listen",
- body: "*"
- }
- }
- ]
- },
- ListCollectionIds: {
- requestType: "ListCollectionIdsRequest",
- responseType: "ListCollectionIdsResponse",
- options: {
- "(google.api.http).post": "/v1/{parent=projects/*/databases/*/documents}:listCollectionIds",
- "(google.api.http).body": "*",
- "(google.api.http).additional_bindings.post": "/v1/{parent=projects/*/databases/*/documents/*/**}:listCollectionIds",
- "(google.api.http).additional_bindings.body": "*",
- "(google.api.method_signature)": "parent"
- },
- parsedOptions: [
- {
- "(google.api.http)": {
- post: "/v1/{parent=projects/*/databases/*/documents}:listCollectionIds",
- body: "*",
- additional_bindings: {
- post: "/v1/{parent=projects/*/databases/*/documents/*/**}:listCollectionIds",
- body: "*"
- }
- }
- },
- {
- "(google.api.method_signature)": "parent"
- }
- ]
- },
- BatchWrite: {
- requestType: "BatchWriteRequest",
- responseType: "BatchWriteResponse",
- options: {
- "(google.api.http).post": "/v1/{database=projects/*/databases/*}/documents:batchWrite",
- "(google.api.http).body": "*"
- },
- parsedOptions: [
- {
- "(google.api.http)": {
- post: "/v1/{database=projects/*/databases/*}/documents:batchWrite",
- body: "*"
- }
- }
- ]
- },
- CreateDocument: {
- requestType: "CreateDocumentRequest",
- responseType: "Document",
- options: {
- "(google.api.http).post": "/v1/{parent=projects/*/databases/*/documents/**}/{collection_id}",
- "(google.api.http).body": "document"
- },
- parsedOptions: [
- {
- "(google.api.http)": {
- post: "/v1/{parent=projects/*/databases/*/documents/**}/{collection_id}",
- body: "document"
- }
- }
- ]
- }
- }
- },
- GetDocumentRequest: {
- oneofs: {
- consistencySelector: {
- oneof: [
- "transaction",
- "readTime"
- ]
- }
- },
- fields: {
- name: {
- type: "string",
- id: 1,
- options: {
- "(google.api.field_behavior)": "REQUIRED"
- }
- },
- mask: {
- type: "DocumentMask",
- id: 2
- },
- transaction: {
- type: "bytes",
- id: 3
- },
- readTime: {
- type: "google.protobuf.Timestamp",
- id: 5
- }
- }
- },
- ListDocumentsRequest: {
- oneofs: {
- consistencySelector: {
- oneof: [
- "transaction",
- "readTime"
- ]
- }
- },
- fields: {
- parent: {
- type: "string",
- id: 1,
- options: {
- "(google.api.field_behavior)": "REQUIRED"
- }
- },
- collectionId: {
- type: "string",
- id: 2,
- options: {
- "(google.api.field_behavior)": "REQUIRED"
- }
- },
- pageSize: {
- type: "int32",
- id: 3
- },
- pageToken: {
- type: "string",
- id: 4
- },
- orderBy: {
- type: "string",
- id: 6
- },
- mask: {
- type: "DocumentMask",
- id: 7
- },
- transaction: {
- type: "bytes",
- id: 8
- },
- readTime: {
- type: "google.protobuf.Timestamp",
- id: 10
- },
- showMissing: {
- type: "bool",
- id: 12
- }
- }
- },
- ListDocumentsResponse: {
- fields: {
- documents: {
- rule: "repeated",
- type: "Document",
- id: 1
- },
- nextPageToken: {
- type: "string",
- id: 2
- }
- }
- },
- CreateDocumentRequest: {
- fields: {
- parent: {
- type: "string",
- id: 1,
- options: {
- "(google.api.field_behavior)": "REQUIRED"
- }
- },
- collectionId: {
- type: "string",
- id: 2,
- options: {
- "(google.api.field_behavior)": "REQUIRED"
- }
- },
- documentId: {
- type: "string",
- id: 3
- },
- document: {
- type: "Document",
- id: 4,
- options: {
- "(google.api.field_behavior)": "REQUIRED"
- }
- },
- mask: {
- type: "DocumentMask",
- id: 5
- }
- }
- },
- UpdateDocumentRequest: {
- fields: {
- document: {
- type: "Document",
- id: 1,
- options: {
- "(google.api.field_behavior)": "REQUIRED"
- }
- },
- updateMask: {
- type: "DocumentMask",
- id: 2
- },
- mask: {
- type: "DocumentMask",
- id: 3
- },
- currentDocument: {
- type: "Precondition",
- id: 4
- }
- }
- },
- DeleteDocumentRequest: {
- fields: {
- name: {
- type: "string",
- id: 1,
- options: {
- "(google.api.field_behavior)": "REQUIRED"
- }
- },
- currentDocument: {
- type: "Precondition",
- id: 2
- }
- }
- },
- BatchGetDocumentsRequest: {
- oneofs: {
- consistencySelector: {
- oneof: [
- "transaction",
- "newTransaction",
- "readTime"
- ]
- }
- },
- fields: {
- database: {
- type: "string",
- id: 1,
- options: {
- "(google.api.field_behavior)": "REQUIRED"
- }
- },
- documents: {
- rule: "repeated",
- type: "string",
- id: 2
- },
- mask: {
- type: "DocumentMask",
- id: 3
- },
- transaction: {
- type: "bytes",
- id: 4
- },
- newTransaction: {
- type: "TransactionOptions",
- id: 5
- },
- readTime: {
- type: "google.protobuf.Timestamp",
- id: 7
- }
- }
- },
- BatchGetDocumentsResponse: {
- oneofs: {
- result: {
- oneof: [
- "found",
- "missing"
- ]
- }
- },
- fields: {
- found: {
- type: "Document",
- id: 1
- },
- missing: {
- type: "string",
- id: 2
- },
- transaction: {
- type: "bytes",
- id: 3
- },
- readTime: {
- type: "google.protobuf.Timestamp",
- id: 4
- }
- }
- },
- BeginTransactionRequest: {
- fields: {
- database: {
- type: "string",
- id: 1,
- options: {
- "(google.api.field_behavior)": "REQUIRED"
- }
- },
- options: {
- type: "TransactionOptions",
- id: 2
- }
- }
- },
- BeginTransactionResponse: {
- fields: {
- transaction: {
- type: "bytes",
- id: 1
- }
- }
- },
- CommitRequest: {
- fields: {
- database: {
- type: "string",
- id: 1,
- options: {
- "(google.api.field_behavior)": "REQUIRED"
- }
- },
- writes: {
- rule: "repeated",
- type: "Write",
- id: 2
- },
- transaction: {
- type: "bytes",
- id: 3
- }
- }
- },
- CommitResponse: {
- fields: {
- writeResults: {
- rule: "repeated",
- type: "WriteResult",
- id: 1
- },
- commitTime: {
- type: "google.protobuf.Timestamp",
- id: 2
- }
- }
- },
- RollbackRequest: {
- fields: {
- database: {
- type: "string",
- id: 1,
- options: {
- "(google.api.field_behavior)": "REQUIRED"
- }
- },
- transaction: {
- type: "bytes",
- id: 2,
- options: {
- "(google.api.field_behavior)": "REQUIRED"
- }
- }
- }
- },
- RunQueryRequest: {
- oneofs: {
- queryType: {
- oneof: [
- "structuredQuery"
- ]
- },
- consistencySelector: {
- oneof: [
- "transaction",
- "newTransaction",
- "readTime"
- ]
- }
- },
- fields: {
- parent: {
- type: "string",
- id: 1,
- options: {
- "(google.api.field_behavior)": "REQUIRED"
- }
- },
- structuredQuery: {
- type: "StructuredQuery",
- id: 2
- },
- transaction: {
- type: "bytes",
- id: 5
- },
- newTransaction: {
- type: "TransactionOptions",
- id: 6
- },
- readTime: {
- type: "google.protobuf.Timestamp",
- id: 7
- }
- }
- },
- RunQueryResponse: {
- fields: {
- transaction: {
- type: "bytes",
- id: 2
- },
- document: {
- type: "Document",
- id: 1
- },
- readTime: {
- type: "google.protobuf.Timestamp",
- id: 3
- },
- skippedResults: {
- type: "int32",
- id: 4
- }
- }
- },
- RunAggregationQueryRequest: {
- oneofs: {
- queryType: {
- oneof: [
- "structuredAggregationQuery"
- ]
- },
- consistencySelector: {
- oneof: [
- "transaction",
- "newTransaction",
- "readTime"
- ]
- }
- },
- fields: {
- parent: {
- type: "string",
- id: 1,
- options: {
- "(google.api.field_behavior)": "REQUIRED"
- }
- },
- structuredAggregationQuery: {
- type: "StructuredAggregationQuery",
- id: 2
- },
- transaction: {
- type: "bytes",
- id: 4
- },
- newTransaction: {
- type: "TransactionOptions",
- id: 5
- },
- readTime: {
- type: "google.protobuf.Timestamp",
- id: 6
- }
- }
- },
- RunAggregationQueryResponse: {
- fields: {
- result: {
- type: "AggregationResult",
- id: 1
- },
- transaction: {
- type: "bytes",
- id: 2
- },
- readTime: {
- type: "google.protobuf.Timestamp",
- id: 3
- }
- }
- },
- PartitionQueryRequest: {
- oneofs: {
- queryType: {
- oneof: [
- "structuredQuery"
- ]
- }
- },
- fields: {
- parent: {
- type: "string",
- id: 1,
- options: {
- "(google.api.field_behavior)": "REQUIRED"
- }
- },
- structuredQuery: {
- type: "StructuredQuery",
- id: 2
- },
- partitionCount: {
- type: "int64",
- id: 3
- },
- pageToken: {
- type: "string",
- id: 4
- },
- pageSize: {
- type: "int32",
- id: 5
- }
- }
- },
- PartitionQueryResponse: {
- fields: {
- partitions: {
- rule: "repeated",
- type: "Cursor",
- id: 1
- },
- nextPageToken: {
- type: "string",
- id: 2
- }
- }
- },
- WriteRequest: {
- fields: {
- database: {
- type: "string",
- id: 1,
- options: {
- "(google.api.field_behavior)": "REQUIRED"
- }
- },
- streamId: {
- type: "string",
- id: 2
- },
- writes: {
- rule: "repeated",
- type: "Write",
- id: 3
- },
- streamToken: {
- type: "bytes",
- id: 4
- },
- labels: {
- keyType: "string",
- type: "string",
- id: 5
- }
- }
- },
- WriteResponse: {
- fields: {
- streamId: {
- type: "string",
- id: 1
- },
- streamToken: {
- type: "bytes",
- id: 2
- },
- writeResults: {
- rule: "repeated",
- type: "WriteResult",
- id: 3
- },
- commitTime: {
- type: "google.protobuf.Timestamp",
- id: 4
- }
- }
- },
- ListenRequest: {
- oneofs: {
- targetChange: {
- oneof: [
- "addTarget",
- "removeTarget"
- ]
- }
- },
- fields: {
- database: {
- type: "string",
- id: 1,
- options: {
- "(google.api.field_behavior)": "REQUIRED"
- }
- },
- addTarget: {
- type: "Target",
- id: 2
- },
- removeTarget: {
- type: "int32",
- id: 3
- },
- labels: {
- keyType: "string",
- type: "string",
- id: 4
- }
- }
- },
- ListenResponse: {
- oneofs: {
- responseType: {
- oneof: [
- "targetChange",
- "documentChange",
- "documentDelete",
- "documentRemove",
- "filter"
- ]
- }
- },
- fields: {
- targetChange: {
- type: "TargetChange",
- id: 2
- },
- documentChange: {
- type: "DocumentChange",
- id: 3
- },
- documentDelete: {
- type: "DocumentDelete",
- id: 4
- },
- documentRemove: {
- type: "DocumentRemove",
- id: 6
- },
- filter: {
- type: "ExistenceFilter",
- id: 5
- }
- }
- },
- Target: {
- oneofs: {
- targetType: {
- oneof: [
- "query",
- "documents"
- ]
- },
- resumeType: {
- oneof: [
- "resumeToken",
- "readTime"
- ]
- }
- },
- fields: {
- query: {
- type: "QueryTarget",
- id: 2
- },
- documents: {
- type: "DocumentsTarget",
- id: 3
- },
- resumeToken: {
- type: "bytes",
- id: 4
- },
- readTime: {
- type: "google.protobuf.Timestamp",
- id: 11
- },
- targetId: {
- type: "int32",
- id: 5
- },
- once: {
- type: "bool",
- id: 6
- },
- expectedCount: {
- type: "google.protobuf.Int32Value",
- id: 12
- }
- },
- nested: {
- DocumentsTarget: {
- fields: {
- documents: {
- rule: "repeated",
- type: "string",
- id: 2
- }
- }
- },
- QueryTarget: {
- oneofs: {
- queryType: {
- oneof: [
- "structuredQuery"
- ]
- }
- },
- fields: {
- parent: {
- type: "string",
- id: 1
- },
- structuredQuery: {
- type: "StructuredQuery",
- id: 2
- }
- }
- }
- }
- },
- TargetChange: {
- fields: {
- targetChangeType: {
- type: "TargetChangeType",
- id: 1
- },
- targetIds: {
- rule: "repeated",
- type: "int32",
- id: 2
- },
- cause: {
- type: "google.rpc.Status",
- id: 3
- },
- resumeToken: {
- type: "bytes",
- id: 4
- },
- readTime: {
- type: "google.protobuf.Timestamp",
- id: 6
- }
- },
- nested: {
- TargetChangeType: {
- values: {
- NO_CHANGE: 0,
- ADD: 1,
- REMOVE: 2,
- CURRENT: 3,
- RESET: 4
- }
- }
- }
- },
- ListCollectionIdsRequest: {
- fields: {
- parent: {
- type: "string",
- id: 1,
- options: {
- "(google.api.field_behavior)": "REQUIRED"
- }
- },
- pageSize: {
- type: "int32",
- id: 2
- },
- pageToken: {
- type: "string",
- id: 3
- }
- }
- },
- ListCollectionIdsResponse: {
- fields: {
- collectionIds: {
- rule: "repeated",
- type: "string",
- id: 1
- },
- nextPageToken: {
- type: "string",
- id: 2
- }
- }
- },
- BatchWriteRequest: {
- fields: {
- database: {
- type: "string",
- id: 1,
- options: {
- "(google.api.field_behavior)": "REQUIRED"
- }
- },
- writes: {
- rule: "repeated",
- type: "Write",
- id: 2
- },
- labels: {
- keyType: "string",
- type: "string",
- id: 3
- }
- }
- },
- BatchWriteResponse: {
- fields: {
- writeResults: {
- rule: "repeated",
- type: "WriteResult",
- id: 1
- },
- status: {
- rule: "repeated",
- type: "google.rpc.Status",
- id: 2
- }
- }
- },
- StructuredQuery: {
- fields: {
- select: {
- type: "Projection",
- id: 1
- },
- from: {
- rule: "repeated",
- type: "CollectionSelector",
- id: 2
- },
- where: {
- type: "Filter",
- id: 3
- },
- orderBy: {
- rule: "repeated",
- type: "Order",
- id: 4
- },
- startAt: {
- type: "Cursor",
- id: 7
- },
- endAt: {
- type: "Cursor",
- id: 8
- },
- offset: {
- type: "int32",
- id: 6
- },
- limit: {
- type: "google.protobuf.Int32Value",
- id: 5
- }
- },
- nested: {
- CollectionSelector: {
- fields: {
- collectionId: {
- type: "string",
- id: 2
- },
- allDescendants: {
- type: "bool",
- id: 3
- }
- }
- },
- Filter: {
- oneofs: {
- filterType: {
- oneof: [
- "compositeFilter",
- "fieldFilter",
- "unaryFilter"
- ]
- }
- },
- fields: {
- compositeFilter: {
- type: "CompositeFilter",
- id: 1
- },
- fieldFilter: {
- type: "FieldFilter",
- id: 2
- },
- unaryFilter: {
- type: "UnaryFilter",
- id: 3
- }
- }
- },
- CompositeFilter: {
- fields: {
- op: {
- type: "Operator",
- id: 1
- },
- filters: {
- rule: "repeated",
- type: "Filter",
- id: 2
- }
- },
- nested: {
- Operator: {
- values: {
- OPERATOR_UNSPECIFIED: 0,
- AND: 1,
- OR: 2
- }
- }
- }
- },
- FieldFilter: {
- fields: {
- field: {
- type: "FieldReference",
- id: 1
- },
- op: {
- type: "Operator",
- id: 2
- },
- value: {
- type: "Value",
- id: 3
- }
- },
- nested: {
- Operator: {
- values: {
- OPERATOR_UNSPECIFIED: 0,
- LESS_THAN: 1,
- LESS_THAN_OR_EQUAL: 2,
- GREATER_THAN: 3,
- GREATER_THAN_OR_EQUAL: 4,
- EQUAL: 5,
- NOT_EQUAL: 6,
- ARRAY_CONTAINS: 7,
- IN: 8,
- ARRAY_CONTAINS_ANY: 9,
- NOT_IN: 10
- }
- }
- }
- },
- UnaryFilter: {
- oneofs: {
- operandType: {
- oneof: [
- "field"
- ]
- }
- },
- fields: {
- op: {
- type: "Operator",
- id: 1
- },
- field: {
- type: "FieldReference",
- id: 2
- }
- },
- nested: {
- Operator: {
- values: {
- OPERATOR_UNSPECIFIED: 0,
- IS_NAN: 2,
- IS_NULL: 3,
- IS_NOT_NAN: 4,
- IS_NOT_NULL: 5
- }
- }
- }
- },
- Order: {
- fields: {
- field: {
- type: "FieldReference",
- id: 1
- },
- direction: {
- type: "Direction",
- id: 2
- }
- }
- },
- FieldReference: {
- fields: {
- fieldPath: {
- type: "string",
- id: 2
- }
- }
- },
- Projection: {
- fields: {
- fields: {
- rule: "repeated",
- type: "FieldReference",
- id: 2
- }
- }
- },
- Direction: {
- values: {
- DIRECTION_UNSPECIFIED: 0,
- ASCENDING: 1,
- DESCENDING: 2
- }
- }
- }
- },
- StructuredAggregationQuery: {
- oneofs: {
- queryType: {
- oneof: [
- "structuredQuery"
- ]
- }
- },
- fields: {
- structuredQuery: {
- type: "StructuredQuery",
- id: 1
- },
- aggregations: {
- rule: "repeated",
- type: "Aggregation",
- id: 3
- }
- },
- nested: {
- Aggregation: {
- oneofs: {
- operator: {
- oneof: [
- "count",
- "sum",
- "avg"
- ]
- }
- },
- fields: {
- count: {
- type: "Count",
- id: 1
- },
- sum: {
- type: "Sum",
- id: 2
- },
- avg: {
- type: "Avg",
- id: 3
- },
- alias: {
- type: "string",
- id: 7
- }
- },
- nested: {
- Count: {
- fields: {
- upTo: {
- type: "google.protobuf.Int64Value",
- id: 1
- }
- }
- },
- Sum: {
- fields: {
- field: {
- type: "FieldReference",
- id: 1
- }
- }
- },
- Avg: {
- fields: {
- field: {
- type: "FieldReference",
- id: 1
- }
- }
- }
- }
- }
- }
- },
- Cursor: {
- fields: {
- values: {
- rule: "repeated",
- type: "Value",
- id: 1
- },
- before: {
- type: "bool",
- id: 2
- }
- }
- },
- Write: {
- oneofs: {
- operation: {
- oneof: [
- "update",
- "delete",
- "verify",
- "transform"
- ]
- }
- },
- fields: {
- update: {
- type: "Document",
- id: 1
- },
- "delete": {
- type: "string",
- id: 2
- },
- verify: {
- type: "string",
- id: 5
- },
- transform: {
- type: "DocumentTransform",
- id: 6
- },
- updateMask: {
- type: "DocumentMask",
- id: 3
- },
- updateTransforms: {
- rule: "repeated",
- type: "DocumentTransform.FieldTransform",
- id: 7
- },
- currentDocument: {
- type: "Precondition",
- id: 4
- }
- }
- },
- DocumentTransform: {
- fields: {
- document: {
- type: "string",
- id: 1
- },
- fieldTransforms: {
- rule: "repeated",
- type: "FieldTransform",
- id: 2
- }
- },
- nested: {
- FieldTransform: {
- oneofs: {
- transformType: {
- oneof: [
- "setToServerValue",
- "increment",
- "maximum",
- "minimum",
- "appendMissingElements",
- "removeAllFromArray"
- ]
- }
- },
- fields: {
- fieldPath: {
- type: "string",
- id: 1
- },
- setToServerValue: {
- type: "ServerValue",
- id: 2
- },
- increment: {
- type: "Value",
- id: 3
- },
- maximum: {
- type: "Value",
- id: 4
- },
- minimum: {
- type: "Value",
- id: 5
- },
- appendMissingElements: {
- type: "ArrayValue",
- id: 6
- },
- removeAllFromArray: {
- type: "ArrayValue",
- id: 7
- }
- },
- nested: {
- ServerValue: {
- values: {
- SERVER_VALUE_UNSPECIFIED: 0,
- REQUEST_TIME: 1
- }
- }
- }
- }
- }
- },
- WriteResult: {
- fields: {
- updateTime: {
- type: "google.protobuf.Timestamp",
- id: 1
- },
- transformResults: {
- rule: "repeated",
- type: "Value",
- id: 2
- }
- }
- },
- DocumentChange: {
- fields: {
- document: {
- type: "Document",
- id: 1
- },
- targetIds: {
- rule: "repeated",
- type: "int32",
- id: 5
- },
- removedTargetIds: {
- rule: "repeated",
- type: "int32",
- id: 6
- }
- }
- },
- DocumentDelete: {
- fields: {
- document: {
- type: "string",
- id: 1
- },
- removedTargetIds: {
- rule: "repeated",
- type: "int32",
- id: 6
- },
- readTime: {
- type: "google.protobuf.Timestamp",
- id: 4
- }
- }
- },
- DocumentRemove: {
- fields: {
- document: {
- type: "string",
- id: 1
- },
- removedTargetIds: {
- rule: "repeated",
- type: "int32",
- id: 2
- },
- readTime: {
- type: "google.protobuf.Timestamp",
- id: 4
- }
- }
- },
- ExistenceFilter: {
- fields: {
- targetId: {
- type: "int32",
- id: 1
- },
- count: {
- type: "int32",
- id: 2
- },
- unchangedNames: {
- type: "BloomFilter",
- id: 3
- }
- }
- }
- }
- }
- }
- },
- api: {
- options: {
- go_package: "google.golang.org/genproto/googleapis/api/annotations;annotations",
- java_multiple_files: true,
- java_outer_classname: "HttpProto",
- java_package: "com.google.api",
- objc_class_prefix: "GAPI",
- cc_enable_arenas: true
- },
- nested: {
- http: {
- type: "HttpRule",
- id: 72295728,
- extend: "google.protobuf.MethodOptions"
- },
- Http: {
- fields: {
- rules: {
- rule: "repeated",
- type: "HttpRule",
- id: 1
- }
- }
- },
- HttpRule: {
- oneofs: {
- pattern: {
- oneof: [
- "get",
- "put",
- "post",
- "delete",
- "patch",
- "custom"
- ]
- }
- },
- fields: {
- get: {
- type: "string",
- id: 2
- },
- put: {
- type: "string",
- id: 3
- },
- post: {
- type: "string",
- id: 4
- },
- "delete": {
- type: "string",
- id: 5
- },
- patch: {
- type: "string",
- id: 6
- },
- custom: {
- type: "CustomHttpPattern",
- id: 8
- },
- selector: {
- type: "string",
- id: 1
- },
- body: {
- type: "string",
- id: 7
- },
- additionalBindings: {
- rule: "repeated",
- type: "HttpRule",
- id: 11
- }
- }
- },
- CustomHttpPattern: {
- fields: {
- kind: {
- type: "string",
- id: 1
- },
- path: {
- type: "string",
- id: 2
- }
- }
- },
- methodSignature: {
- rule: "repeated",
- type: "string",
- id: 1051,
- extend: "google.protobuf.MethodOptions"
- },
- defaultHost: {
- type: "string",
- id: 1049,
- extend: "google.protobuf.ServiceOptions"
- },
- oauthScopes: {
- type: "string",
- id: 1050,
- extend: "google.protobuf.ServiceOptions"
- },
- fieldBehavior: {
- rule: "repeated",
- type: "google.api.FieldBehavior",
- id: 1052,
- extend: "google.protobuf.FieldOptions"
- },
- FieldBehavior: {
- values: {
- FIELD_BEHAVIOR_UNSPECIFIED: 0,
- OPTIONAL: 1,
- REQUIRED: 2,
- OUTPUT_ONLY: 3,
- INPUT_ONLY: 4,
- IMMUTABLE: 5,
- UNORDERED_LIST: 6,
- NON_EMPTY_DEFAULT: 7
- }
- }
- }
- },
- type: {
- options: {
- cc_enable_arenas: true,
- go_package: "google.golang.org/genproto/googleapis/type/latlng;latlng",
- java_multiple_files: true,
- java_outer_classname: "LatLngProto",
- java_package: "com.google.type",
- objc_class_prefix: "GTP"
- },
- nested: {
- LatLng: {
- fields: {
- latitude: {
- type: "double",
- id: 1
- },
- longitude: {
- type: "double",
- id: 2
- }
- }
- }
- }
- },
- rpc: {
- options: {
- cc_enable_arenas: true,
- go_package: "google.golang.org/genproto/googleapis/rpc/status;status",
- java_multiple_files: true,
- java_outer_classname: "StatusProto",
- java_package: "com.google.rpc",
- objc_class_prefix: "RPC"
- },
- nested: {
- Status: {
- fields: {
- code: {
- type: "int32",
- id: 1
- },
- message: {
- type: "string",
- id: 2
- },
- details: {
- rule: "repeated",
- type: "google.protobuf.Any",
- id: 3
- }
- }
- }
- }
- }
- }
- }
- };
- var protos = {
- nested: nested
- };
- var protos$1 = /*#__PURE__*/Object.freeze({
- __proto__: null,
- nested: nested,
- 'default': protos
- });
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /** Used by tests so we can match @grpc/proto-loader behavior. */
- const protoLoaderOptions = {
- longs: String,
- enums: String,
- defaults: true,
- oneofs: false
- };
- /**
- * Loads the protocol buffer definitions for Firestore.
- *
- * @returns The GrpcObject representing our protos.
- */
- function loadProtos() {
- const packageDefinition = protoLoader__namespace.fromJSON(protos$1, protoLoaderOptions);
- return grpc__namespace.loadPackageDefinition(packageDefinition);
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /** Loads the GRPC stack */
- function newConnection(databaseInfo) {
- const protos = loadProtos();
- return new GrpcConnection(protos, databaseInfo);
- }
- /** Return the Platform-specific connectivity monitor. */
- function newConnectivityMonitor() {
- return new NoopConnectivityMonitor();
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /** The Platform's 'window' implementation or null if not available. */
- function getWindow() {
- if (process.env.USE_MOCK_PERSISTENCE === 'YES') {
- // eslint-disable-next-line no-restricted-globals
- return window;
- }
- return null;
- }
- /** The Platform's 'document' implementation or null if not available. */
- function getDocument() {
- return null;
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- function newSerializer(databaseId) {
- return new JsonProtoSerializer(databaseId, /* useProto3Json= */ false);
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const LOG_TAG$8 = 'ExponentialBackoff';
- /**
- * Initial backoff time in milliseconds after an error.
- * Set to 1s according to https://cloud.google.com/apis/design/errors.
- */
- const DEFAULT_BACKOFF_INITIAL_DELAY_MS = 1000;
- const DEFAULT_BACKOFF_FACTOR = 1.5;
- /** Maximum backoff time in milliseconds */
- const DEFAULT_BACKOFF_MAX_DELAY_MS = 60 * 1000;
- /**
- * A helper for running delayed tasks following an exponential backoff curve
- * between attempts.
- *
- * Each delay is made up of a "base" delay which follows the exponential
- * backoff curve, and a +/- 50% "jitter" that is calculated and added to the
- * base delay. This prevents clients from accidentally synchronizing their
- * delays causing spikes of load to the backend.
- */
- class ExponentialBackoff {
- constructor(
- /**
- * The AsyncQueue to run backoff operations on.
- */
- queue,
- /**
- * The ID to use when scheduling backoff operations on the AsyncQueue.
- */
- timerId,
- /**
- * The initial delay (used as the base delay on the first retry attempt).
- * Note that jitter will still be applied, so the actual delay could be as
- * little as 0.5*initialDelayMs.
- */
- initialDelayMs = DEFAULT_BACKOFF_INITIAL_DELAY_MS,
- /**
- * The multiplier to use to determine the extended base delay after each
- * attempt.
- */
- backoffFactor = DEFAULT_BACKOFF_FACTOR,
- /**
- * The maximum base delay after which no further backoff is performed.
- * Note that jitter will still be applied, so the actual delay could be as
- * much as 1.5*maxDelayMs.
- */
- maxDelayMs = DEFAULT_BACKOFF_MAX_DELAY_MS) {
- this.queue = queue;
- this.timerId = timerId;
- this.initialDelayMs = initialDelayMs;
- this.backoffFactor = backoffFactor;
- this.maxDelayMs = maxDelayMs;
- this.currentBaseMs = 0;
- this.timerPromise = null;
- /** The last backoff attempt, as epoch milliseconds. */
- this.lastAttemptTime = Date.now();
- this.reset();
- }
- /**
- * Resets the backoff delay.
- *
- * The very next backoffAndWait() will have no delay. If it is called again
- * (i.e. due to an error), initialDelayMs (plus jitter) will be used, and
- * subsequent ones will increase according to the backoffFactor.
- */
- reset() {
- this.currentBaseMs = 0;
- }
- /**
- * Resets the backoff delay to the maximum delay (e.g. for use after a
- * RESOURCE_EXHAUSTED error).
- */
- resetToMax() {
- this.currentBaseMs = this.maxDelayMs;
- }
- /**
- * Returns a promise that resolves after currentDelayMs, and increases the
- * delay for any subsequent attempts. If there was a pending backoff operation
- * already, it will be canceled.
- */
- backoffAndRun(op) {
- // Cancel any pending backoff operation.
- this.cancel();
- // First schedule using the current base (which may be 0 and should be
- // honored as such).
- const desiredDelayWithJitterMs = Math.floor(this.currentBaseMs + this.jitterDelayMs());
- // Guard against lastAttemptTime being in the future due to a clock change.
- const delaySoFarMs = Math.max(0, Date.now() - this.lastAttemptTime);
- // Guard against the backoff delay already being past.
- const remainingDelayMs = Math.max(0, desiredDelayWithJitterMs - delaySoFarMs);
- if (remainingDelayMs > 0) {
- logDebug(LOG_TAG$8, `Backing off for ${remainingDelayMs} ms ` +
- `(base delay: ${this.currentBaseMs} ms, ` +
- `delay with jitter: ${desiredDelayWithJitterMs} ms, ` +
- `last attempt: ${delaySoFarMs} ms ago)`);
- }
- this.timerPromise = this.queue.enqueueAfterDelay(this.timerId, remainingDelayMs, () => {
- this.lastAttemptTime = Date.now();
- return op();
- });
- // Apply backoff factor to determine next delay and ensure it is within
- // bounds.
- this.currentBaseMs *= this.backoffFactor;
- if (this.currentBaseMs < this.initialDelayMs) {
- this.currentBaseMs = this.initialDelayMs;
- }
- if (this.currentBaseMs > this.maxDelayMs) {
- this.currentBaseMs = this.maxDelayMs;
- }
- }
- skipBackoff() {
- if (this.timerPromise !== null) {
- this.timerPromise.skipDelay();
- this.timerPromise = null;
- }
- }
- cancel() {
- if (this.timerPromise !== null) {
- this.timerPromise.cancel();
- this.timerPromise = null;
- }
- }
- /** Returns a random value in the range [-currentBaseMs/2, currentBaseMs/2] */
- jitterDelayMs() {
- return (Math.random() - 0.5) * this.currentBaseMs;
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const LOG_TAG$7 = 'PersistentStream';
- /** The time a stream stays open after it is marked idle. */
- const IDLE_TIMEOUT_MS = 60 * 1000;
- /** The time a stream stays open until we consider it healthy. */
- const HEALTHY_TIMEOUT_MS = 10 * 1000;
- /**
- * A PersistentStream is an abstract base class that represents a streaming RPC
- * to the Firestore backend. It's built on top of the connections own support
- * for streaming RPCs, and adds several critical features for our clients:
- *
- * - Exponential backoff on failure
- * - Authentication via CredentialsProvider
- * - Dispatching all callbacks into the shared worker queue
- * - Closing idle streams after 60 seconds of inactivity
- *
- * Subclasses of PersistentStream implement serialization of models to and
- * from the JSON representation of the protocol buffers for a specific
- * streaming RPC.
- *
- * ## Starting and Stopping
- *
- * Streaming RPCs are stateful and need to be start()ed before messages can
- * be sent and received. The PersistentStream will call the onOpen() function
- * of the listener once the stream is ready to accept requests.
- *
- * Should a start() fail, PersistentStream will call the registered onClose()
- * listener with a FirestoreError indicating what went wrong.
- *
- * A PersistentStream can be started and stopped repeatedly.
- *
- * Generic types:
- * SendType: The type of the outgoing message of the underlying
- * connection stream
- * ReceiveType: The type of the incoming message of the underlying
- * connection stream
- * ListenerType: The type of the listener that will be used for callbacks
- */
- class PersistentStream {
- constructor(queue, connectionTimerId, idleTimerId, healthTimerId, connection, authCredentialsProvider, appCheckCredentialsProvider, listener) {
- this.queue = queue;
- this.idleTimerId = idleTimerId;
- this.healthTimerId = healthTimerId;
- this.connection = connection;
- this.authCredentialsProvider = authCredentialsProvider;
- this.appCheckCredentialsProvider = appCheckCredentialsProvider;
- this.listener = listener;
- this.state = 0 /* PersistentStreamState.Initial */;
- /**
- * A close count that's incremented every time the stream is closed; used by
- * getCloseGuardedDispatcher() to invalidate callbacks that happen after
- * close.
- */
- this.closeCount = 0;
- this.idleTimer = null;
- this.healthCheck = null;
- this.stream = null;
- this.backoff = new ExponentialBackoff(queue, connectionTimerId);
- }
- /**
- * Returns true if start() has been called and no error has occurred. True
- * indicates the stream is open or in the process of opening (which
- * encompasses respecting backoff, getting auth tokens, and starting the
- * actual RPC). Use isOpen() to determine if the stream is open and ready for
- * outbound requests.
- */
- isStarted() {
- return (this.state === 1 /* PersistentStreamState.Starting */ ||
- this.state === 5 /* PersistentStreamState.Backoff */ ||
- this.isOpen());
- }
- /**
- * Returns true if the underlying RPC is open (the onOpen() listener has been
- * called) and the stream is ready for outbound requests.
- */
- isOpen() {
- return (this.state === 2 /* PersistentStreamState.Open */ ||
- this.state === 3 /* PersistentStreamState.Healthy */);
- }
- /**
- * Starts the RPC. Only allowed if isStarted() returns false. The stream is
- * not immediately ready for use: onOpen() will be invoked when the RPC is
- * ready for outbound requests, at which point isOpen() will return true.
- *
- * When start returns, isStarted() will return true.
- */
- start() {
- if (this.state === 4 /* PersistentStreamState.Error */) {
- this.performBackoff();
- return;
- }
- this.auth();
- }
- /**
- * Stops the RPC. This call is idempotent and allowed regardless of the
- * current isStarted() state.
- *
- * When stop returns, isStarted() and isOpen() will both return false.
- */
- async stop() {
- if (this.isStarted()) {
- await this.close(0 /* PersistentStreamState.Initial */);
- }
- }
- /**
- * After an error the stream will usually back off on the next attempt to
- * start it. If the error warrants an immediate restart of the stream, the
- * sender can use this to indicate that the receiver should not back off.
- *
- * Each error will call the onClose() listener. That function can decide to
- * inhibit backoff if required.
- */
- inhibitBackoff() {
- this.state = 0 /* PersistentStreamState.Initial */;
- this.backoff.reset();
- }
- /**
- * Marks this stream as idle. If no further actions are performed on the
- * stream for one minute, the stream will automatically close itself and
- * notify the stream's onClose() handler with Status.OK. The stream will then
- * be in a !isStarted() state, requiring the caller to start the stream again
- * before further use.
- *
- * Only streams that are in state 'Open' can be marked idle, as all other
- * states imply pending network operations.
- */
- markIdle() {
- // Starts the idle time if we are in state 'Open' and are not yet already
- // running a timer (in which case the previous idle timeout still applies).
- if (this.isOpen() && this.idleTimer === null) {
- this.idleTimer = this.queue.enqueueAfterDelay(this.idleTimerId, IDLE_TIMEOUT_MS, () => this.handleIdleCloseTimer());
- }
- }
- /** Sends a message to the underlying stream. */
- sendRequest(msg) {
- this.cancelIdleCheck();
- this.stream.send(msg);
- }
- /** Called by the idle timer when the stream should close due to inactivity. */
- async handleIdleCloseTimer() {
- if (this.isOpen()) {
- // When timing out an idle stream there's no reason to force the stream into backoff when
- // it restarts so set the stream state to Initial instead of Error.
- return this.close(0 /* PersistentStreamState.Initial */);
- }
- }
- /** Marks the stream as active again. */
- cancelIdleCheck() {
- if (this.idleTimer) {
- this.idleTimer.cancel();
- this.idleTimer = null;
- }
- }
- /** Cancels the health check delayed operation. */
- cancelHealthCheck() {
- if (this.healthCheck) {
- this.healthCheck.cancel();
- this.healthCheck = null;
- }
- }
- /**
- * Closes the stream and cleans up as necessary:
- *
- * * closes the underlying GRPC stream;
- * * calls the onClose handler with the given 'error';
- * * sets internal stream state to 'finalState';
- * * adjusts the backoff timer based on the error
- *
- * A new stream can be opened by calling start().
- *
- * @param finalState - the intended state of the stream after closing.
- * @param error - the error the connection was closed with.
- */
- async close(finalState, error) {
- // Cancel any outstanding timers (they're guaranteed not to execute).
- this.cancelIdleCheck();
- this.cancelHealthCheck();
- this.backoff.cancel();
- // Invalidates any stream-related callbacks (e.g. from auth or the
- // underlying stream), guaranteeing they won't execute.
- this.closeCount++;
- if (finalState !== 4 /* PersistentStreamState.Error */) {
- // If this is an intentional close ensure we don't delay our next connection attempt.
- this.backoff.reset();
- }
- else if (error && error.code === Code.RESOURCE_EXHAUSTED) {
- // Log the error. (Probably either 'quota exceeded' or 'max queue length reached'.)
- logError(error.toString());
- logError('Using maximum backoff delay to prevent overloading the backend.');
- this.backoff.resetToMax();
- }
- else if (error &&
- error.code === Code.UNAUTHENTICATED &&
- this.state !== 3 /* PersistentStreamState.Healthy */) {
- // "unauthenticated" error means the token was rejected. This should rarely
- // happen since both Auth and AppCheck ensure a sufficient TTL when we
- // request a token. If a user manually resets their system clock this can
- // fail, however. In this case, we should get a Code.UNAUTHENTICATED error
- // before we received the first message and we need to invalidate the token
- // to ensure that we fetch a new token.
- this.authCredentialsProvider.invalidateToken();
- this.appCheckCredentialsProvider.invalidateToken();
- }
- // Clean up the underlying stream because we are no longer interested in events.
- if (this.stream !== null) {
- this.tearDown();
- this.stream.close();
- this.stream = null;
- }
- // This state must be assigned before calling onClose() to allow the callback to
- // inhibit backoff or otherwise manipulate the state in its non-started state.
- this.state = finalState;
- // Notify the listener that the stream closed.
- await this.listener.onClose(error);
- }
- /**
- * Can be overridden to perform additional cleanup before the stream is closed.
- * Calling super.tearDown() is not required.
- */
- tearDown() { }
- auth() {
- this.state = 1 /* PersistentStreamState.Starting */;
- const dispatchIfNotClosed = this.getCloseGuardedDispatcher(this.closeCount);
- // TODO(mikelehen): Just use dispatchIfNotClosed, but see TODO below.
- const closeCount = this.closeCount;
- Promise.all([
- this.authCredentialsProvider.getToken(),
- this.appCheckCredentialsProvider.getToken()
- ]).then(([authToken, appCheckToken]) => {
- // Stream can be stopped while waiting for authentication.
- // TODO(mikelehen): We really should just use dispatchIfNotClosed
- // and let this dispatch onto the queue, but that opened a spec test can
- // of worms that I don't want to deal with in this PR.
- if (this.closeCount === closeCount) {
- // Normally we'd have to schedule the callback on the AsyncQueue.
- // However, the following calls are safe to be called outside the
- // AsyncQueue since they don't chain asynchronous calls
- this.startStream(authToken, appCheckToken);
- }
- }, (error) => {
- dispatchIfNotClosed(() => {
- const rpcError = new FirestoreError(Code.UNKNOWN, 'Fetching auth token failed: ' + error.message);
- return this.handleStreamClose(rpcError);
- });
- });
- }
- startStream(authToken, appCheckToken) {
- const dispatchIfNotClosed = this.getCloseGuardedDispatcher(this.closeCount);
- this.stream = this.startRpc(authToken, appCheckToken);
- this.stream.onOpen(() => {
- dispatchIfNotClosed(() => {
- this.state = 2 /* PersistentStreamState.Open */;
- this.healthCheck = this.queue.enqueueAfterDelay(this.healthTimerId, HEALTHY_TIMEOUT_MS, () => {
- if (this.isOpen()) {
- this.state = 3 /* PersistentStreamState.Healthy */;
- }
- return Promise.resolve();
- });
- return this.listener.onOpen();
- });
- });
- this.stream.onClose((error) => {
- dispatchIfNotClosed(() => {
- return this.handleStreamClose(error);
- });
- });
- this.stream.onMessage((msg) => {
- dispatchIfNotClosed(() => {
- return this.onMessage(msg);
- });
- });
- }
- performBackoff() {
- this.state = 5 /* PersistentStreamState.Backoff */;
- this.backoff.backoffAndRun(async () => {
- this.state = 0 /* PersistentStreamState.Initial */;
- this.start();
- });
- }
- // Visible for tests
- handleStreamClose(error) {
- logDebug(LOG_TAG$7, `close with error: ${error}`);
- this.stream = null;
- // In theory the stream could close cleanly, however, in our current model
- // we never expect this to happen because if we stop a stream ourselves,
- // this callback will never be called. To prevent cases where we retry
- // without a backoff accidentally, we set the stream to error in all cases.
- return this.close(4 /* PersistentStreamState.Error */, error);
- }
- /**
- * Returns a "dispatcher" function that dispatches operations onto the
- * AsyncQueue but only runs them if closeCount remains unchanged. This allows
- * us to turn auth / stream callbacks into no-ops if the stream is closed /
- * re-opened, etc.
- */
- getCloseGuardedDispatcher(startCloseCount) {
- return (fn) => {
- this.queue.enqueueAndForget(() => {
- if (this.closeCount === startCloseCount) {
- return fn();
- }
- else {
- logDebug(LOG_TAG$7, 'stream callback skipped by getCloseGuardedDispatcher.');
- return Promise.resolve();
- }
- });
- };
- }
- }
- /**
- * A PersistentStream that implements the Listen RPC.
- *
- * Once the Listen stream has called the onOpen() listener, any number of
- * listen() and unlisten() calls can be made to control what changes will be
- * sent from the server for ListenResponses.
- */
- class PersistentListenStream extends PersistentStream {
- constructor(queue, connection, authCredentials, appCheckCredentials, serializer, listener) {
- super(queue, "listen_stream_connection_backoff" /* TimerId.ListenStreamConnectionBackoff */, "listen_stream_idle" /* TimerId.ListenStreamIdle */, "health_check_timeout" /* TimerId.HealthCheckTimeout */, connection, authCredentials, appCheckCredentials, listener);
- this.serializer = serializer;
- }
- startRpc(authToken, appCheckToken) {
- return this.connection.openStream('Listen', authToken, appCheckToken);
- }
- onMessage(watchChangeProto) {
- // A successful response means the stream is healthy
- this.backoff.reset();
- const watchChange = fromWatchChange(this.serializer, watchChangeProto);
- const snapshot = versionFromListenResponse(watchChangeProto);
- return this.listener.onWatchChange(watchChange, snapshot);
- }
- /**
- * Registers interest in the results of the given target. If the target
- * includes a resumeToken it will be included in the request. Results that
- * affect the target will be streamed back as WatchChange messages that
- * reference the targetId.
- */
- watch(targetData) {
- const request = {};
- request.database = getEncodedDatabaseId(this.serializer);
- request.addTarget = toTarget(this.serializer, targetData);
- const labels = toListenRequestLabels(this.serializer, targetData);
- if (labels) {
- request.labels = labels;
- }
- this.sendRequest(request);
- }
- /**
- * Unregisters interest in the results of the target associated with the
- * given targetId.
- */
- unwatch(targetId) {
- const request = {};
- request.database = getEncodedDatabaseId(this.serializer);
- request.removeTarget = targetId;
- this.sendRequest(request);
- }
- }
- /**
- * A Stream that implements the Write RPC.
- *
- * The Write RPC requires the caller to maintain special streamToken
- * state in between calls, to help the server understand which responses the
- * client has processed by the time the next request is made. Every response
- * will contain a streamToken; this value must be passed to the next
- * request.
- *
- * After calling start() on this stream, the next request must be a handshake,
- * containing whatever streamToken is on hand. Once a response to this
- * request is received, all pending mutations may be submitted. When
- * submitting multiple batches of mutations at the same time, it's
- * okay to use the same streamToken for the calls to writeMutations.
- *
- * TODO(b/33271235): Use proto types
- */
- class PersistentWriteStream extends PersistentStream {
- constructor(queue, connection, authCredentials, appCheckCredentials, serializer, listener) {
- super(queue, "write_stream_connection_backoff" /* TimerId.WriteStreamConnectionBackoff */, "write_stream_idle" /* TimerId.WriteStreamIdle */, "health_check_timeout" /* TimerId.HealthCheckTimeout */, connection, authCredentials, appCheckCredentials, listener);
- this.serializer = serializer;
- this.handshakeComplete_ = false;
- }
- /**
- * Tracks whether or not a handshake has been successfully exchanged and
- * the stream is ready to accept mutations.
- */
- get handshakeComplete() {
- return this.handshakeComplete_;
- }
- // Override of PersistentStream.start
- start() {
- this.handshakeComplete_ = false;
- this.lastStreamToken = undefined;
- super.start();
- }
- tearDown() {
- if (this.handshakeComplete_) {
- this.writeMutations([]);
- }
- }
- startRpc(authToken, appCheckToken) {
- return this.connection.openStream('Write', authToken, appCheckToken);
- }
- onMessage(responseProto) {
- // Always capture the last stream token.
- hardAssert(!!responseProto.streamToken);
- this.lastStreamToken = responseProto.streamToken;
- if (!this.handshakeComplete_) {
- // The first response is always the handshake response
- hardAssert(!responseProto.writeResults || responseProto.writeResults.length === 0);
- this.handshakeComplete_ = true;
- return this.listener.onHandshakeComplete();
- }
- else {
- // A successful first write response means the stream is healthy,
- // Note, that we could consider a successful handshake healthy, however,
- // the write itself might be causing an error we want to back off from.
- this.backoff.reset();
- const results = fromWriteResults(responseProto.writeResults, responseProto.commitTime);
- const commitVersion = fromVersion(responseProto.commitTime);
- return this.listener.onMutationResult(commitVersion, results);
- }
- }
- /**
- * Sends an initial streamToken to the server, performing the handshake
- * required to make the StreamingWrite RPC work. Subsequent
- * calls should wait until onHandshakeComplete was called.
- */
- writeHandshake() {
- // TODO(dimond): Support stream resumption. We intentionally do not set the
- // stream token on the handshake, ignoring any stream token we might have.
- const request = {};
- request.database = getEncodedDatabaseId(this.serializer);
- this.sendRequest(request);
- }
- /** Sends a group of mutations to the Firestore backend to apply. */
- writeMutations(mutations) {
- const request = {
- streamToken: this.lastStreamToken,
- writes: mutations.map(mutation => toMutation(this.serializer, mutation))
- };
- this.sendRequest(request);
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Datastore and its related methods are a wrapper around the external Google
- * Cloud Datastore grpc API, which provides an interface that is more convenient
- * for the rest of the client SDK architecture to consume.
- */
- class Datastore {
- }
- /**
- * An implementation of Datastore that exposes additional state for internal
- * consumption.
- */
- class DatastoreImpl extends Datastore {
- constructor(authCredentials, appCheckCredentials, connection, serializer) {
- super();
- this.authCredentials = authCredentials;
- this.appCheckCredentials = appCheckCredentials;
- this.connection = connection;
- this.serializer = serializer;
- this.terminated = false;
- }
- verifyInitialized() {
- if (this.terminated) {
- throw new FirestoreError(Code.FAILED_PRECONDITION, 'The client has already been terminated.');
- }
- }
- /** Invokes the provided RPC with auth and AppCheck tokens. */
- invokeRPC(rpcName, path, request) {
- this.verifyInitialized();
- return Promise.all([
- this.authCredentials.getToken(),
- this.appCheckCredentials.getToken()
- ])
- .then(([authToken, appCheckToken]) => {
- return this.connection.invokeRPC(rpcName, path, request, authToken, appCheckToken);
- })
- .catch((error) => {
- if (error.name === 'FirebaseError') {
- if (error.code === Code.UNAUTHENTICATED) {
- this.authCredentials.invalidateToken();
- this.appCheckCredentials.invalidateToken();
- }
- throw error;
- }
- else {
- throw new FirestoreError(Code.UNKNOWN, error.toString());
- }
- });
- }
- /** Invokes the provided RPC with streamed results with auth and AppCheck tokens. */
- invokeStreamingRPC(rpcName, path, request, expectedResponseCount) {
- this.verifyInitialized();
- return Promise.all([
- this.authCredentials.getToken(),
- this.appCheckCredentials.getToken()
- ])
- .then(([authToken, appCheckToken]) => {
- return this.connection.invokeStreamingRPC(rpcName, path, request, authToken, appCheckToken, expectedResponseCount);
- })
- .catch((error) => {
- if (error.name === 'FirebaseError') {
- if (error.code === Code.UNAUTHENTICATED) {
- this.authCredentials.invalidateToken();
- this.appCheckCredentials.invalidateToken();
- }
- throw error;
- }
- else {
- throw new FirestoreError(Code.UNKNOWN, error.toString());
- }
- });
- }
- terminate() {
- this.terminated = true;
- }
- }
- // TODO(firestorexp): Make sure there is only one Datastore instance per
- // firestore-exp client.
- function newDatastore(authCredentials, appCheckCredentials, connection, serializer) {
- return new DatastoreImpl(authCredentials, appCheckCredentials, connection, serializer);
- }
- async function invokeCommitRpc(datastore, mutations) {
- const datastoreImpl = debugCast(datastore);
- const path = getEncodedDatabaseId(datastoreImpl.serializer) + '/documents';
- const request = {
- writes: mutations.map(m => toMutation(datastoreImpl.serializer, m))
- };
- await datastoreImpl.invokeRPC('Commit', path, request);
- }
- async function invokeBatchGetDocumentsRpc(datastore, keys) {
- const datastoreImpl = debugCast(datastore);
- const path = getEncodedDatabaseId(datastoreImpl.serializer) + '/documents';
- const request = {
- documents: keys.map(k => toName(datastoreImpl.serializer, k))
- };
- const response = await datastoreImpl.invokeStreamingRPC('BatchGetDocuments', path, request, keys.length);
- const docs = new Map();
- response.forEach(proto => {
- const doc = fromBatchGetDocumentsResponse(datastoreImpl.serializer, proto);
- docs.set(doc.key.toString(), doc);
- });
- const result = [];
- keys.forEach(key => {
- const doc = docs.get(key.toString());
- hardAssert(!!doc);
- result.push(doc);
- });
- return result;
- }
- async function invokeRunAggregationQueryRpc(datastore, query, aggregates) {
- var _a;
- const datastoreImpl = debugCast(datastore);
- const { request, aliasMap } = toRunAggregationQueryRequest(datastoreImpl.serializer, queryToTarget(query), aggregates);
- const parent = request.parent;
- if (!datastoreImpl.connection.shouldResourcePathBeIncludedInRequest) {
- delete request.parent;
- }
- const response = await datastoreImpl.invokeStreamingRPC('RunAggregationQuery', parent, request, /*expectedResponseCount=*/ 1);
- // Omit RunAggregationQueryResponse that only contain readTimes.
- const filteredResult = response.filter(proto => !!proto.result);
- hardAssert(filteredResult.length === 1);
- // Remap the short-form aliases that were sent to the server
- // to the client-side aliases. Users will access the results
- // using the client-side alias.
- const unmappedAggregateFields = (_a = filteredResult[0].result) === null || _a === void 0 ? void 0 : _a.aggregateFields;
- const remappedFields = Object.keys(unmappedAggregateFields).reduce((accumulator, key) => {
- accumulator[aliasMap[key]] = unmappedAggregateFields[key];
- return accumulator;
- }, {});
- return remappedFields;
- }
- function newPersistentWriteStream(datastore, queue, listener) {
- const datastoreImpl = debugCast(datastore);
- datastoreImpl.verifyInitialized();
- return new PersistentWriteStream(queue, datastoreImpl.connection, datastoreImpl.authCredentials, datastoreImpl.appCheckCredentials, datastoreImpl.serializer, listener);
- }
- function newPersistentWatchStream(datastore, queue, listener) {
- const datastoreImpl = debugCast(datastore);
- datastoreImpl.verifyInitialized();
- return new PersistentListenStream(queue, datastoreImpl.connection, datastoreImpl.authCredentials, datastoreImpl.appCheckCredentials, datastoreImpl.serializer, listener);
- }
- /**
- * @license
- * Copyright 2018 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const LOG_TAG$6 = 'OnlineStateTracker';
- // To deal with transient failures, we allow multiple stream attempts before
- // giving up and transitioning from OnlineState.Unknown to Offline.
- // TODO(mikelehen): This used to be set to 2 as a mitigation for b/66228394.
- // @jdimond thinks that bug is sufficiently fixed so that we can set this back
- // to 1. If that works okay, we could potentially remove this logic entirely.
- const MAX_WATCH_STREAM_FAILURES = 1;
- // To deal with stream attempts that don't succeed or fail in a timely manner,
- // we have a timeout for OnlineState to reach Online or Offline.
- // If the timeout is reached, we transition to Offline rather than waiting
- // indefinitely.
- const ONLINE_STATE_TIMEOUT_MS = 10 * 1000;
- /**
- * A component used by the RemoteStore to track the OnlineState (that is,
- * whether or not the client as a whole should be considered to be online or
- * offline), implementing the appropriate heuristics.
- *
- * In particular, when the client is trying to connect to the backend, we
- * allow up to MAX_WATCH_STREAM_FAILURES within ONLINE_STATE_TIMEOUT_MS for
- * a connection to succeed. If we have too many failures or the timeout elapses,
- * then we set the OnlineState to Offline, and the client will behave as if
- * it is offline (get()s will return cached data, etc.).
- */
- class OnlineStateTracker {
- constructor(asyncQueue, onlineStateHandler) {
- this.asyncQueue = asyncQueue;
- this.onlineStateHandler = onlineStateHandler;
- /** The current OnlineState. */
- this.state = "Unknown" /* OnlineState.Unknown */;
- /**
- * A count of consecutive failures to open the stream. If it reaches the
- * maximum defined by MAX_WATCH_STREAM_FAILURES, we'll set the OnlineState to
- * Offline.
- */
- this.watchStreamFailures = 0;
- /**
- * A timer that elapses after ONLINE_STATE_TIMEOUT_MS, at which point we
- * transition from OnlineState.Unknown to OnlineState.Offline without waiting
- * for the stream to actually fail (MAX_WATCH_STREAM_FAILURES times).
- */
- this.onlineStateTimer = null;
- /**
- * Whether the client should log a warning message if it fails to connect to
- * the backend (initially true, cleared after a successful stream, or if we've
- * logged the message already).
- */
- this.shouldWarnClientIsOffline = true;
- }
- /**
- * Called by RemoteStore when a watch stream is started (including on each
- * backoff attempt).
- *
- * If this is the first attempt, it sets the OnlineState to Unknown and starts
- * the onlineStateTimer.
- */
- handleWatchStreamStart() {
- if (this.watchStreamFailures === 0) {
- this.setAndBroadcast("Unknown" /* OnlineState.Unknown */);
- this.onlineStateTimer = this.asyncQueue.enqueueAfterDelay("online_state_timeout" /* TimerId.OnlineStateTimeout */, ONLINE_STATE_TIMEOUT_MS, () => {
- this.onlineStateTimer = null;
- this.logClientOfflineWarningIfNecessary(`Backend didn't respond within ${ONLINE_STATE_TIMEOUT_MS / 1000} ` +
- `seconds.`);
- this.setAndBroadcast("Offline" /* OnlineState.Offline */);
- // NOTE: handleWatchStreamFailure() will continue to increment
- // watchStreamFailures even though we are already marked Offline,
- // but this is non-harmful.
- return Promise.resolve();
- });
- }
- }
- /**
- * Updates our OnlineState as appropriate after the watch stream reports a
- * failure. The first failure moves us to the 'Unknown' state. We then may
- * allow multiple failures (based on MAX_WATCH_STREAM_FAILURES) before we
- * actually transition to the 'Offline' state.
- */
- handleWatchStreamFailure(error) {
- if (this.state === "Online" /* OnlineState.Online */) {
- this.setAndBroadcast("Unknown" /* OnlineState.Unknown */);
- }
- else {
- this.watchStreamFailures++;
- if (this.watchStreamFailures >= MAX_WATCH_STREAM_FAILURES) {
- this.clearOnlineStateTimer();
- this.logClientOfflineWarningIfNecessary(`Connection failed ${MAX_WATCH_STREAM_FAILURES} ` +
- `times. Most recent error: ${error.toString()}`);
- this.setAndBroadcast("Offline" /* OnlineState.Offline */);
- }
- }
- }
- /**
- * Explicitly sets the OnlineState to the specified state.
- *
- * Note that this resets our timers / failure counters, etc. used by our
- * Offline heuristics, so must not be used in place of
- * handleWatchStreamStart() and handleWatchStreamFailure().
- */
- set(newState) {
- this.clearOnlineStateTimer();
- this.watchStreamFailures = 0;
- if (newState === "Online" /* OnlineState.Online */) {
- // We've connected to watch at least once. Don't warn the developer
- // about being offline going forward.
- this.shouldWarnClientIsOffline = false;
- }
- this.setAndBroadcast(newState);
- }
- setAndBroadcast(newState) {
- if (newState !== this.state) {
- this.state = newState;
- this.onlineStateHandler(newState);
- }
- }
- logClientOfflineWarningIfNecessary(details) {
- const message = `Could not reach Cloud Firestore backend. ${details}\n` +
- `This typically indicates that your device does not have a healthy ` +
- `Internet connection at the moment. The client will operate in offline ` +
- `mode until it is able to successfully connect to the backend.`;
- if (this.shouldWarnClientIsOffline) {
- logError(message);
- this.shouldWarnClientIsOffline = false;
- }
- else {
- logDebug(LOG_TAG$6, message);
- }
- }
- clearOnlineStateTimer() {
- if (this.onlineStateTimer !== null) {
- this.onlineStateTimer.cancel();
- this.onlineStateTimer = null;
- }
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const LOG_TAG$5 = 'RemoteStore';
- // TODO(b/35853402): Negotiate this with the stream.
- const MAX_PENDING_WRITES = 10;
- class RemoteStoreImpl {
- constructor(
- /**
- * The local store, used to fill the write pipeline with outbound mutations.
- */
- localStore,
- /** The client-side proxy for interacting with the backend. */
- datastore, asyncQueue, onlineStateHandler, connectivityMonitor) {
- this.localStore = localStore;
- this.datastore = datastore;
- this.asyncQueue = asyncQueue;
- this.remoteSyncer = {};
- /**
- * A list of up to MAX_PENDING_WRITES writes that we have fetched from the
- * LocalStore via fillWritePipeline() and have or will send to the write
- * stream.
- *
- * Whenever writePipeline.length > 0 the RemoteStore will attempt to start or
- * restart the write stream. When the stream is established the writes in the
- * pipeline will be sent in order.
- *
- * Writes remain in writePipeline until they are acknowledged by the backend
- * and thus will automatically be re-sent if the stream is interrupted /
- * restarted before they're acknowledged.
- *
- * Write responses from the backend are linked to their originating request
- * purely based on order, and so we can just shift() writes from the front of
- * the writePipeline as we receive responses.
- */
- this.writePipeline = [];
- /**
- * A mapping of watched targets that the client cares about tracking and the
- * user has explicitly called a 'listen' for this target.
- *
- * These targets may or may not have been sent to or acknowledged by the
- * server. On re-establishing the listen stream, these targets should be sent
- * to the server. The targets removed with unlistens are removed eagerly
- * without waiting for confirmation from the listen stream.
- */
- this.listenTargets = new Map();
- /**
- * A set of reasons for why the RemoteStore may be offline. If empty, the
- * RemoteStore may start its network connections.
- */
- this.offlineCauses = new Set();
- /**
- * Event handlers that get called when the network is disabled or enabled.
- *
- * PORTING NOTE: These functions are used on the Web client to create the
- * underlying streams (to support tree-shakeable streams). On Android and iOS,
- * the streams are created during construction of RemoteStore.
- */
- this.onNetworkStatusChange = [];
- this.connectivityMonitor = connectivityMonitor;
- this.connectivityMonitor.addCallback((_) => {
- asyncQueue.enqueueAndForget(async () => {
- // Porting Note: Unlike iOS, `restartNetwork()` is called even when the
- // network becomes unreachable as we don't have any other way to tear
- // down our streams.
- if (canUseNetwork(this)) {
- logDebug(LOG_TAG$5, 'Restarting streams for network reachability change.');
- await restartNetwork(this);
- }
- });
- });
- this.onlineStateTracker = new OnlineStateTracker(asyncQueue, onlineStateHandler);
- }
- }
- function newRemoteStore(localStore, datastore, asyncQueue, onlineStateHandler, connectivityMonitor) {
- return new RemoteStoreImpl(localStore, datastore, asyncQueue, onlineStateHandler, connectivityMonitor);
- }
- /** Re-enables the network. Idempotent. */
- function remoteStoreEnableNetwork(remoteStore) {
- const remoteStoreImpl = debugCast(remoteStore);
- remoteStoreImpl.offlineCauses.delete(0 /* OfflineCause.UserDisabled */);
- return enableNetworkInternal(remoteStoreImpl);
- }
- async function enableNetworkInternal(remoteStoreImpl) {
- if (canUseNetwork(remoteStoreImpl)) {
- for (const networkStatusHandler of remoteStoreImpl.onNetworkStatusChange) {
- await networkStatusHandler(/* enabled= */ true);
- }
- }
- }
- /**
- * Temporarily disables the network. The network can be re-enabled using
- * enableNetwork().
- */
- async function remoteStoreDisableNetwork(remoteStore) {
- const remoteStoreImpl = debugCast(remoteStore);
- remoteStoreImpl.offlineCauses.add(0 /* OfflineCause.UserDisabled */);
- await disableNetworkInternal(remoteStoreImpl);
- // Set the OnlineState to Offline so get()s return from cache, etc.
- remoteStoreImpl.onlineStateTracker.set("Offline" /* OnlineState.Offline */);
- }
- async function disableNetworkInternal(remoteStoreImpl) {
- for (const networkStatusHandler of remoteStoreImpl.onNetworkStatusChange) {
- await networkStatusHandler(/* enabled= */ false);
- }
- }
- async function remoteStoreShutdown(remoteStore) {
- const remoteStoreImpl = debugCast(remoteStore);
- logDebug(LOG_TAG$5, 'RemoteStore shutting down.');
- remoteStoreImpl.offlineCauses.add(5 /* OfflineCause.Shutdown */);
- await disableNetworkInternal(remoteStoreImpl);
- remoteStoreImpl.connectivityMonitor.shutdown();
- // Set the OnlineState to Unknown (rather than Offline) to avoid potentially
- // triggering spurious listener events with cached data, etc.
- remoteStoreImpl.onlineStateTracker.set("Unknown" /* OnlineState.Unknown */);
- }
- /**
- * Starts new listen for the given target. Uses resume token if provided. It
- * is a no-op if the target of given `TargetData` is already being listened to.
- */
- function remoteStoreListen(remoteStore, targetData) {
- const remoteStoreImpl = debugCast(remoteStore);
- if (remoteStoreImpl.listenTargets.has(targetData.targetId)) {
- return;
- }
- // Mark this as something the client is currently listening for.
- remoteStoreImpl.listenTargets.set(targetData.targetId, targetData);
- if (shouldStartWatchStream(remoteStoreImpl)) {
- // The listen will be sent in onWatchStreamOpen
- startWatchStream(remoteStoreImpl);
- }
- else if (ensureWatchStream(remoteStoreImpl).isOpen()) {
- sendWatchRequest(remoteStoreImpl, targetData);
- }
- }
- /**
- * Removes the listen from server. It is a no-op if the given target id is
- * not being listened to.
- */
- function remoteStoreUnlisten(remoteStore, targetId) {
- const remoteStoreImpl = debugCast(remoteStore);
- const watchStream = ensureWatchStream(remoteStoreImpl);
- remoteStoreImpl.listenTargets.delete(targetId);
- if (watchStream.isOpen()) {
- sendUnwatchRequest(remoteStoreImpl, targetId);
- }
- if (remoteStoreImpl.listenTargets.size === 0) {
- if (watchStream.isOpen()) {
- watchStream.markIdle();
- }
- else if (canUseNetwork(remoteStoreImpl)) {
- // Revert to OnlineState.Unknown if the watch stream is not open and we
- // have no listeners, since without any listens to send we cannot
- // confirm if the stream is healthy and upgrade to OnlineState.Online.
- remoteStoreImpl.onlineStateTracker.set("Unknown" /* OnlineState.Unknown */);
- }
- }
- }
- /**
- * We need to increment the the expected number of pending responses we're due
- * from watch so we wait for the ack to process any messages from this target.
- */
- function sendWatchRequest(remoteStoreImpl, targetData) {
- remoteStoreImpl.watchChangeAggregator.recordPendingTargetRequest(targetData.targetId);
- if (targetData.resumeToken.approximateByteSize() > 0 ||
- targetData.snapshotVersion.compareTo(SnapshotVersion.min()) > 0) {
- const expectedCount = remoteStoreImpl.remoteSyncer.getRemoteKeysForTarget(targetData.targetId).size;
- targetData = targetData.withExpectedCount(expectedCount);
- }
- ensureWatchStream(remoteStoreImpl).watch(targetData);
- }
- /**
- * We need to increment the expected number of pending responses we're due
- * from watch so we wait for the removal on the server before we process any
- * messages from this target.
- */
- function sendUnwatchRequest(remoteStoreImpl, targetId) {
- remoteStoreImpl.watchChangeAggregator.recordPendingTargetRequest(targetId);
- ensureWatchStream(remoteStoreImpl).unwatch(targetId);
- }
- function startWatchStream(remoteStoreImpl) {
- remoteStoreImpl.watchChangeAggregator = new WatchChangeAggregator({
- getRemoteKeysForTarget: targetId => remoteStoreImpl.remoteSyncer.getRemoteKeysForTarget(targetId),
- getTargetDataForTarget: targetId => remoteStoreImpl.listenTargets.get(targetId) || null,
- getDatabaseId: () => remoteStoreImpl.datastore.serializer.databaseId
- });
- ensureWatchStream(remoteStoreImpl).start();
- remoteStoreImpl.onlineStateTracker.handleWatchStreamStart();
- }
- /**
- * Returns whether the watch stream should be started because it's necessary
- * and has not yet been started.
- */
- function shouldStartWatchStream(remoteStoreImpl) {
- return (canUseNetwork(remoteStoreImpl) &&
- !ensureWatchStream(remoteStoreImpl).isStarted() &&
- remoteStoreImpl.listenTargets.size > 0);
- }
- function canUseNetwork(remoteStore) {
- const remoteStoreImpl = debugCast(remoteStore);
- return remoteStoreImpl.offlineCauses.size === 0;
- }
- function cleanUpWatchStreamState(remoteStoreImpl) {
- remoteStoreImpl.watchChangeAggregator = undefined;
- }
- async function onWatchStreamOpen(remoteStoreImpl) {
- remoteStoreImpl.listenTargets.forEach((targetData, targetId) => {
- sendWatchRequest(remoteStoreImpl, targetData);
- });
- }
- async function onWatchStreamClose(remoteStoreImpl, error) {
- cleanUpWatchStreamState(remoteStoreImpl);
- // If we still need the watch stream, retry the connection.
- if (shouldStartWatchStream(remoteStoreImpl)) {
- remoteStoreImpl.onlineStateTracker.handleWatchStreamFailure(error);
- startWatchStream(remoteStoreImpl);
- }
- else {
- // No need to restart watch stream because there are no active targets.
- // The online state is set to unknown because there is no active attempt
- // at establishing a connection
- remoteStoreImpl.onlineStateTracker.set("Unknown" /* OnlineState.Unknown */);
- }
- }
- async function onWatchStreamChange(remoteStoreImpl, watchChange, snapshotVersion) {
- // Mark the client as online since we got a message from the server
- remoteStoreImpl.onlineStateTracker.set("Online" /* OnlineState.Online */);
- if (watchChange instanceof WatchTargetChange &&
- watchChange.state === 2 /* WatchTargetChangeState.Removed */ &&
- watchChange.cause) {
- // There was an error on a target, don't wait for a consistent snapshot
- // to raise events
- try {
- await handleTargetError(remoteStoreImpl, watchChange);
- }
- catch (e) {
- logDebug(LOG_TAG$5, 'Failed to remove targets %s: %s ', watchChange.targetIds.join(','), e);
- await disableNetworkUntilRecovery(remoteStoreImpl, e);
- }
- return;
- }
- if (watchChange instanceof DocumentWatchChange) {
- remoteStoreImpl.watchChangeAggregator.handleDocumentChange(watchChange);
- }
- else if (watchChange instanceof ExistenceFilterChange) {
- remoteStoreImpl.watchChangeAggregator.handleExistenceFilter(watchChange);
- }
- else {
- remoteStoreImpl.watchChangeAggregator.handleTargetChange(watchChange);
- }
- if (!snapshotVersion.isEqual(SnapshotVersion.min())) {
- try {
- const lastRemoteSnapshotVersion = await localStoreGetLastRemoteSnapshotVersion(remoteStoreImpl.localStore);
- if (snapshotVersion.compareTo(lastRemoteSnapshotVersion) >= 0) {
- // We have received a target change with a global snapshot if the snapshot
- // version is not equal to SnapshotVersion.min().
- await raiseWatchSnapshot(remoteStoreImpl, snapshotVersion);
- }
- }
- catch (e) {
- logDebug(LOG_TAG$5, 'Failed to raise snapshot:', e);
- await disableNetworkUntilRecovery(remoteStoreImpl, e);
- }
- }
- }
- /**
- * Recovery logic for IndexedDB errors that takes the network offline until
- * `op` succeeds. Retries are scheduled with backoff using
- * `enqueueRetryable()`. If `op()` is not provided, IndexedDB access is
- * validated via a generic operation.
- *
- * The returned Promise is resolved once the network is disabled and before
- * any retry attempt.
- */
- async function disableNetworkUntilRecovery(remoteStoreImpl, e, op) {
- if (isIndexedDbTransactionError(e)) {
- remoteStoreImpl.offlineCauses.add(1 /* OfflineCause.IndexedDbFailed */);
- // Disable network and raise offline snapshots
- await disableNetworkInternal(remoteStoreImpl);
- remoteStoreImpl.onlineStateTracker.set("Offline" /* OnlineState.Offline */);
- if (!op) {
- // Use a simple read operation to determine if IndexedDB recovered.
- // Ideally, we would expose a health check directly on SimpleDb, but
- // RemoteStore only has access to persistence through LocalStore.
- op = () => localStoreGetLastRemoteSnapshotVersion(remoteStoreImpl.localStore);
- }
- // Probe IndexedDB periodically and re-enable network
- remoteStoreImpl.asyncQueue.enqueueRetryable(async () => {
- logDebug(LOG_TAG$5, 'Retrying IndexedDB access');
- await op();
- remoteStoreImpl.offlineCauses.delete(1 /* OfflineCause.IndexedDbFailed */);
- await enableNetworkInternal(remoteStoreImpl);
- });
- }
- else {
- throw e;
- }
- }
- /**
- * Executes `op`. If `op` fails, takes the network offline until `op`
- * succeeds. Returns after the first attempt.
- */
- function executeWithRecovery(remoteStoreImpl, op) {
- return op().catch(e => disableNetworkUntilRecovery(remoteStoreImpl, e, op));
- }
- /**
- * Takes a batch of changes from the Datastore, repackages them as a
- * RemoteEvent, and passes that on to the listener, which is typically the
- * SyncEngine.
- */
- function raiseWatchSnapshot(remoteStoreImpl, snapshotVersion) {
- const remoteEvent = remoteStoreImpl.watchChangeAggregator.createRemoteEvent(snapshotVersion);
- // Update in-memory resume tokens. LocalStore will update the
- // persistent view of these when applying the completed RemoteEvent.
- remoteEvent.targetChanges.forEach((change, targetId) => {
- if (change.resumeToken.approximateByteSize() > 0) {
- const targetData = remoteStoreImpl.listenTargets.get(targetId);
- // A watched target might have been removed already.
- if (targetData) {
- remoteStoreImpl.listenTargets.set(targetId, targetData.withResumeToken(change.resumeToken, snapshotVersion));
- }
- }
- });
- // Re-establish listens for the targets that have been invalidated by
- // existence filter mismatches.
- remoteEvent.targetMismatches.forEach((targetId, targetPurpose) => {
- const targetData = remoteStoreImpl.listenTargets.get(targetId);
- if (!targetData) {
- // A watched target might have been removed already.
- return;
- }
- // Clear the resume token for the target, since we're in a known mismatch
- // state.
- remoteStoreImpl.listenTargets.set(targetId, targetData.withResumeToken(ByteString.EMPTY_BYTE_STRING, targetData.snapshotVersion));
- // Cause a hard reset by unwatching and rewatching immediately, but
- // deliberately don't send a resume token so that we get a full update.
- sendUnwatchRequest(remoteStoreImpl, targetId);
- // Mark the target we send as being on behalf of an existence filter
- // mismatch, but don't actually retain that in listenTargets. This ensures
- // that we flag the first re-listen this way without impacting future
- // listens of this target (that might happen e.g. on reconnect).
- const requestTargetData = new TargetData(targetData.target, targetId, targetPurpose, targetData.sequenceNumber);
- sendWatchRequest(remoteStoreImpl, requestTargetData);
- });
- return remoteStoreImpl.remoteSyncer.applyRemoteEvent(remoteEvent);
- }
- /** Handles an error on a target */
- async function handleTargetError(remoteStoreImpl, watchChange) {
- const error = watchChange.cause;
- for (const targetId of watchChange.targetIds) {
- // A watched target might have been removed already.
- if (remoteStoreImpl.listenTargets.has(targetId)) {
- await remoteStoreImpl.remoteSyncer.rejectListen(targetId, error);
- remoteStoreImpl.listenTargets.delete(targetId);
- remoteStoreImpl.watchChangeAggregator.removeTarget(targetId);
- }
- }
- }
- /**
- * Attempts to fill our write pipeline with writes from the LocalStore.
- *
- * Called internally to bootstrap or refill the write pipeline and by
- * SyncEngine whenever there are new mutations to process.
- *
- * Starts the write stream if necessary.
- */
- async function fillWritePipeline(remoteStore) {
- const remoteStoreImpl = debugCast(remoteStore);
- const writeStream = ensureWriteStream(remoteStoreImpl);
- let lastBatchIdRetrieved = remoteStoreImpl.writePipeline.length > 0
- ? remoteStoreImpl.writePipeline[remoteStoreImpl.writePipeline.length - 1]
- .batchId
- : BATCHID_UNKNOWN;
- while (canAddToWritePipeline(remoteStoreImpl)) {
- try {
- const batch = await localStoreGetNextMutationBatch(remoteStoreImpl.localStore, lastBatchIdRetrieved);
- if (batch === null) {
- if (remoteStoreImpl.writePipeline.length === 0) {
- writeStream.markIdle();
- }
- break;
- }
- else {
- lastBatchIdRetrieved = batch.batchId;
- addToWritePipeline(remoteStoreImpl, batch);
- }
- }
- catch (e) {
- await disableNetworkUntilRecovery(remoteStoreImpl, e);
- }
- }
- if (shouldStartWriteStream(remoteStoreImpl)) {
- startWriteStream(remoteStoreImpl);
- }
- }
- /**
- * Returns true if we can add to the write pipeline (i.e. the network is
- * enabled and the write pipeline is not full).
- */
- function canAddToWritePipeline(remoteStoreImpl) {
- return (canUseNetwork(remoteStoreImpl) &&
- remoteStoreImpl.writePipeline.length < MAX_PENDING_WRITES);
- }
- /**
- * Queues additional writes to be sent to the write stream, sending them
- * immediately if the write stream is established.
- */
- function addToWritePipeline(remoteStoreImpl, batch) {
- remoteStoreImpl.writePipeline.push(batch);
- const writeStream = ensureWriteStream(remoteStoreImpl);
- if (writeStream.isOpen() && writeStream.handshakeComplete) {
- writeStream.writeMutations(batch.mutations);
- }
- }
- function shouldStartWriteStream(remoteStoreImpl) {
- return (canUseNetwork(remoteStoreImpl) &&
- !ensureWriteStream(remoteStoreImpl).isStarted() &&
- remoteStoreImpl.writePipeline.length > 0);
- }
- function startWriteStream(remoteStoreImpl) {
- ensureWriteStream(remoteStoreImpl).start();
- }
- async function onWriteStreamOpen(remoteStoreImpl) {
- ensureWriteStream(remoteStoreImpl).writeHandshake();
- }
- async function onWriteHandshakeComplete(remoteStoreImpl) {
- const writeStream = ensureWriteStream(remoteStoreImpl);
- // Send the write pipeline now that the stream is established.
- for (const batch of remoteStoreImpl.writePipeline) {
- writeStream.writeMutations(batch.mutations);
- }
- }
- async function onMutationResult(remoteStoreImpl, commitVersion, results) {
- const batch = remoteStoreImpl.writePipeline.shift();
- const success = MutationBatchResult.from(batch, commitVersion, results);
- await executeWithRecovery(remoteStoreImpl, () => remoteStoreImpl.remoteSyncer.applySuccessfulWrite(success));
- // It's possible that with the completion of this mutation another
- // slot has freed up.
- await fillWritePipeline(remoteStoreImpl);
- }
- async function onWriteStreamClose(remoteStoreImpl, error) {
- // If the write stream closed after the write handshake completes, a write
- // operation failed and we fail the pending operation.
- if (error && ensureWriteStream(remoteStoreImpl).handshakeComplete) {
- // This error affects the actual write.
- await handleWriteError(remoteStoreImpl, error);
- }
- // The write stream might have been started by refilling the write
- // pipeline for failed writes
- if (shouldStartWriteStream(remoteStoreImpl)) {
- startWriteStream(remoteStoreImpl);
- }
- }
- async function handleWriteError(remoteStoreImpl, error) {
- // Only handle permanent errors here. If it's transient, just let the retry
- // logic kick in.
- if (isPermanentWriteError(error.code)) {
- // This was a permanent error, the request itself was the problem
- // so it's not going to succeed if we resend it.
- const batch = remoteStoreImpl.writePipeline.shift();
- // In this case it's also unlikely that the server itself is melting
- // down -- this was just a bad request so inhibit backoff on the next
- // restart.
- ensureWriteStream(remoteStoreImpl).inhibitBackoff();
- await executeWithRecovery(remoteStoreImpl, () => remoteStoreImpl.remoteSyncer.rejectFailedWrite(batch.batchId, error));
- // It's possible that with the completion of this mutation
- // another slot has freed up.
- await fillWritePipeline(remoteStoreImpl);
- }
- }
- async function restartNetwork(remoteStore) {
- const remoteStoreImpl = debugCast(remoteStore);
- remoteStoreImpl.offlineCauses.add(4 /* OfflineCause.ConnectivityChange */);
- await disableNetworkInternal(remoteStoreImpl);
- remoteStoreImpl.onlineStateTracker.set("Unknown" /* OnlineState.Unknown */);
- remoteStoreImpl.offlineCauses.delete(4 /* OfflineCause.ConnectivityChange */);
- await enableNetworkInternal(remoteStoreImpl);
- }
- async function remoteStoreHandleCredentialChange(remoteStore, user) {
- const remoteStoreImpl = debugCast(remoteStore);
- remoteStoreImpl.asyncQueue.verifyOperationInProgress();
- logDebug(LOG_TAG$5, 'RemoteStore received new credentials');
- const usesNetwork = canUseNetwork(remoteStoreImpl);
- // Tear down and re-create our network streams. This will ensure we get a
- // fresh auth token for the new user and re-fill the write pipeline with
- // new mutations from the LocalStore (since mutations are per-user).
- remoteStoreImpl.offlineCauses.add(3 /* OfflineCause.CredentialChange */);
- await disableNetworkInternal(remoteStoreImpl);
- if (usesNetwork) {
- // Don't set the network status to Unknown if we are offline.
- remoteStoreImpl.onlineStateTracker.set("Unknown" /* OnlineState.Unknown */);
- }
- await remoteStoreImpl.remoteSyncer.handleCredentialChange(user);
- remoteStoreImpl.offlineCauses.delete(3 /* OfflineCause.CredentialChange */);
- await enableNetworkInternal(remoteStoreImpl);
- }
- /**
- * Toggles the network state when the client gains or loses its primary lease.
- */
- async function remoteStoreApplyPrimaryState(remoteStore, isPrimary) {
- const remoteStoreImpl = debugCast(remoteStore);
- if (isPrimary) {
- remoteStoreImpl.offlineCauses.delete(2 /* OfflineCause.IsSecondary */);
- await enableNetworkInternal(remoteStoreImpl);
- }
- else if (!isPrimary) {
- remoteStoreImpl.offlineCauses.add(2 /* OfflineCause.IsSecondary */);
- await disableNetworkInternal(remoteStoreImpl);
- remoteStoreImpl.onlineStateTracker.set("Unknown" /* OnlineState.Unknown */);
- }
- }
- /**
- * If not yet initialized, registers the WatchStream and its network state
- * callback with `remoteStoreImpl`. Returns the existing stream if one is
- * already available.
- *
- * PORTING NOTE: On iOS and Android, the WatchStream gets registered on startup.
- * This is not done on Web to allow it to be tree-shaken.
- */
- function ensureWatchStream(remoteStoreImpl) {
- if (!remoteStoreImpl.watchStream) {
- // Create stream (but note that it is not started yet).
- remoteStoreImpl.watchStream = newPersistentWatchStream(remoteStoreImpl.datastore, remoteStoreImpl.asyncQueue, {
- onOpen: onWatchStreamOpen.bind(null, remoteStoreImpl),
- onClose: onWatchStreamClose.bind(null, remoteStoreImpl),
- onWatchChange: onWatchStreamChange.bind(null, remoteStoreImpl)
- });
- remoteStoreImpl.onNetworkStatusChange.push(async (enabled) => {
- if (enabled) {
- remoteStoreImpl.watchStream.inhibitBackoff();
- if (shouldStartWatchStream(remoteStoreImpl)) {
- startWatchStream(remoteStoreImpl);
- }
- else {
- remoteStoreImpl.onlineStateTracker.set("Unknown" /* OnlineState.Unknown */);
- }
- }
- else {
- await remoteStoreImpl.watchStream.stop();
- cleanUpWatchStreamState(remoteStoreImpl);
- }
- });
- }
- return remoteStoreImpl.watchStream;
- }
- /**
- * If not yet initialized, registers the WriteStream and its network state
- * callback with `remoteStoreImpl`. Returns the existing stream if one is
- * already available.
- *
- * PORTING NOTE: On iOS and Android, the WriteStream gets registered on startup.
- * This is not done on Web to allow it to be tree-shaken.
- */
- function ensureWriteStream(remoteStoreImpl) {
- if (!remoteStoreImpl.writeStream) {
- // Create stream (but note that it is not started yet).
- remoteStoreImpl.writeStream = newPersistentWriteStream(remoteStoreImpl.datastore, remoteStoreImpl.asyncQueue, {
- onOpen: onWriteStreamOpen.bind(null, remoteStoreImpl),
- onClose: onWriteStreamClose.bind(null, remoteStoreImpl),
- onHandshakeComplete: onWriteHandshakeComplete.bind(null, remoteStoreImpl),
- onMutationResult: onMutationResult.bind(null, remoteStoreImpl)
- });
- remoteStoreImpl.onNetworkStatusChange.push(async (enabled) => {
- if (enabled) {
- remoteStoreImpl.writeStream.inhibitBackoff();
- // This will start the write stream if necessary.
- await fillWritePipeline(remoteStoreImpl);
- }
- else {
- await remoteStoreImpl.writeStream.stop();
- if (remoteStoreImpl.writePipeline.length > 0) {
- logDebug(LOG_TAG$5, `Stopping write stream with ${remoteStoreImpl.writePipeline.length} pending writes`);
- remoteStoreImpl.writePipeline = [];
- }
- }
- });
- }
- return remoteStoreImpl.writeStream;
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const LOG_TAG$4 = 'AsyncQueue';
- /**
- * Represents an operation scheduled to be run in the future on an AsyncQueue.
- *
- * It is created via DelayedOperation.createAndSchedule().
- *
- * Supports cancellation (via cancel()) and early execution (via skipDelay()).
- *
- * Note: We implement `PromiseLike` instead of `Promise`, as the `Promise` type
- * in newer versions of TypeScript defines `finally`, which is not available in
- * IE.
- */
- class DelayedOperation {
- constructor(asyncQueue, timerId, targetTimeMs, op, removalCallback) {
- this.asyncQueue = asyncQueue;
- this.timerId = timerId;
- this.targetTimeMs = targetTimeMs;
- this.op = op;
- this.removalCallback = removalCallback;
- this.deferred = new Deferred();
- this.then = this.deferred.promise.then.bind(this.deferred.promise);
- // It's normal for the deferred promise to be canceled (due to cancellation)
- // and so we attach a dummy catch callback to avoid
- // 'UnhandledPromiseRejectionWarning' log spam.
- this.deferred.promise.catch(err => { });
- }
- /**
- * Creates and returns a DelayedOperation that has been scheduled to be
- * executed on the provided asyncQueue after the provided delayMs.
- *
- * @param asyncQueue - The queue to schedule the operation on.
- * @param id - A Timer ID identifying the type of operation this is.
- * @param delayMs - The delay (ms) before the operation should be scheduled.
- * @param op - The operation to run.
- * @param removalCallback - A callback to be called synchronously once the
- * operation is executed or canceled, notifying the AsyncQueue to remove it
- * from its delayedOperations list.
- * PORTING NOTE: This exists to prevent making removeDelayedOperation() and
- * the DelayedOperation class public.
- */
- static createAndSchedule(asyncQueue, timerId, delayMs, op, removalCallback) {
- const targetTime = Date.now() + delayMs;
- const delayedOp = new DelayedOperation(asyncQueue, timerId, targetTime, op, removalCallback);
- delayedOp.start(delayMs);
- return delayedOp;
- }
- /**
- * Starts the timer. This is called immediately after construction by
- * createAndSchedule().
- */
- start(delayMs) {
- this.timerHandle = setTimeout(() => this.handleDelayElapsed(), delayMs);
- }
- /**
- * Queues the operation to run immediately (if it hasn't already been run or
- * canceled).
- */
- skipDelay() {
- return this.handleDelayElapsed();
- }
- /**
- * Cancels the operation if it hasn't already been executed or canceled. The
- * promise will be rejected.
- *
- * As long as the operation has not yet been run, calling cancel() provides a
- * guarantee that the operation will not be run.
- */
- cancel(reason) {
- if (this.timerHandle !== null) {
- this.clearTimeout();
- this.deferred.reject(new FirestoreError(Code.CANCELLED, 'Operation cancelled' + (reason ? ': ' + reason : '')));
- }
- }
- handleDelayElapsed() {
- this.asyncQueue.enqueueAndForget(() => {
- if (this.timerHandle !== null) {
- this.clearTimeout();
- return this.op().then(result => {
- return this.deferred.resolve(result);
- });
- }
- else {
- return Promise.resolve();
- }
- });
- }
- clearTimeout() {
- if (this.timerHandle !== null) {
- this.removalCallback(this);
- clearTimeout(this.timerHandle);
- this.timerHandle = null;
- }
- }
- }
- /**
- * Returns a FirestoreError that can be surfaced to the user if the provided
- * error is an IndexedDbTransactionError. Re-throws the error otherwise.
- */
- function wrapInUserErrorIfRecoverable(e, msg) {
- logError(LOG_TAG$4, `${msg}: ${e}`);
- if (isIndexedDbTransactionError(e)) {
- return new FirestoreError(Code.UNAVAILABLE, `${msg}: ${e}`);
- }
- else {
- throw e;
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * DocumentSet is an immutable (copy-on-write) collection that holds documents
- * in order specified by the provided comparator. We always add a document key
- * comparator on top of what is provided to guarantee document equality based on
- * the key.
- */
- class DocumentSet {
- /** The default ordering is by key if the comparator is omitted */
- constructor(comp) {
- // We are adding document key comparator to the end as it's the only
- // guaranteed unique property of a document.
- if (comp) {
- this.comparator = (d1, d2) => comp(d1, d2) || DocumentKey.comparator(d1.key, d2.key);
- }
- else {
- this.comparator = (d1, d2) => DocumentKey.comparator(d1.key, d2.key);
- }
- this.keyedMap = documentMap();
- this.sortedSet = new SortedMap(this.comparator);
- }
- /**
- * Returns an empty copy of the existing DocumentSet, using the same
- * comparator.
- */
- static emptySet(oldSet) {
- return new DocumentSet(oldSet.comparator);
- }
- has(key) {
- return this.keyedMap.get(key) != null;
- }
- get(key) {
- return this.keyedMap.get(key);
- }
- first() {
- return this.sortedSet.minKey();
- }
- last() {
- return this.sortedSet.maxKey();
- }
- isEmpty() {
- return this.sortedSet.isEmpty();
- }
- /**
- * Returns the index of the provided key in the document set, or -1 if the
- * document key is not present in the set;
- */
- indexOf(key) {
- const doc = this.keyedMap.get(key);
- return doc ? this.sortedSet.indexOf(doc) : -1;
- }
- get size() {
- return this.sortedSet.size;
- }
- /** Iterates documents in order defined by "comparator" */
- forEach(cb) {
- this.sortedSet.inorderTraversal((k, v) => {
- cb(k);
- return false;
- });
- }
- /** Inserts or updates a document with the same key */
- add(doc) {
- // First remove the element if we have it.
- const set = this.delete(doc.key);
- return set.copy(set.keyedMap.insert(doc.key, doc), set.sortedSet.insert(doc, null));
- }
- /** Deletes a document with a given key */
- delete(key) {
- const doc = this.get(key);
- if (!doc) {
- return this;
- }
- return this.copy(this.keyedMap.remove(key), this.sortedSet.remove(doc));
- }
- isEqual(other) {
- if (!(other instanceof DocumentSet)) {
- return false;
- }
- if (this.size !== other.size) {
- return false;
- }
- const thisIt = this.sortedSet.getIterator();
- const otherIt = other.sortedSet.getIterator();
- while (thisIt.hasNext()) {
- const thisDoc = thisIt.getNext().key;
- const otherDoc = otherIt.getNext().key;
- if (!thisDoc.isEqual(otherDoc)) {
- return false;
- }
- }
- return true;
- }
- toString() {
- const docStrings = [];
- this.forEach(doc => {
- docStrings.push(doc.toString());
- });
- if (docStrings.length === 0) {
- return 'DocumentSet ()';
- }
- else {
- return 'DocumentSet (\n ' + docStrings.join(' \n') + '\n)';
- }
- }
- copy(keyedMap, sortedSet) {
- const newSet = new DocumentSet();
- newSet.comparator = this.comparator;
- newSet.keyedMap = keyedMap;
- newSet.sortedSet = sortedSet;
- return newSet;
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * DocumentChangeSet keeps track of a set of changes to docs in a query, merging
- * duplicate events for the same doc.
- */
- class DocumentChangeSet {
- constructor() {
- this.changeMap = new SortedMap(DocumentKey.comparator);
- }
- track(change) {
- const key = change.doc.key;
- const oldChange = this.changeMap.get(key);
- if (!oldChange) {
- this.changeMap = this.changeMap.insert(key, change);
- return;
- }
- // Merge the new change with the existing change.
- if (change.type !== 0 /* ChangeType.Added */ &&
- oldChange.type === 3 /* ChangeType.Metadata */) {
- this.changeMap = this.changeMap.insert(key, change);
- }
- else if (change.type === 3 /* ChangeType.Metadata */ &&
- oldChange.type !== 1 /* ChangeType.Removed */) {
- this.changeMap = this.changeMap.insert(key, {
- type: oldChange.type,
- doc: change.doc
- });
- }
- else if (change.type === 2 /* ChangeType.Modified */ &&
- oldChange.type === 2 /* ChangeType.Modified */) {
- this.changeMap = this.changeMap.insert(key, {
- type: 2 /* ChangeType.Modified */,
- doc: change.doc
- });
- }
- else if (change.type === 2 /* ChangeType.Modified */ &&
- oldChange.type === 0 /* ChangeType.Added */) {
- this.changeMap = this.changeMap.insert(key, {
- type: 0 /* ChangeType.Added */,
- doc: change.doc
- });
- }
- else if (change.type === 1 /* ChangeType.Removed */ &&
- oldChange.type === 0 /* ChangeType.Added */) {
- this.changeMap = this.changeMap.remove(key);
- }
- else if (change.type === 1 /* ChangeType.Removed */ &&
- oldChange.type === 2 /* ChangeType.Modified */) {
- this.changeMap = this.changeMap.insert(key, {
- type: 1 /* ChangeType.Removed */,
- doc: oldChange.doc
- });
- }
- else if (change.type === 0 /* ChangeType.Added */ &&
- oldChange.type === 1 /* ChangeType.Removed */) {
- this.changeMap = this.changeMap.insert(key, {
- type: 2 /* ChangeType.Modified */,
- doc: change.doc
- });
- }
- else {
- // This includes these cases, which don't make sense:
- // Added->Added
- // Removed->Removed
- // Modified->Added
- // Removed->Modified
- // Metadata->Added
- // Removed->Metadata
- fail();
- }
- }
- getChanges() {
- const changes = [];
- this.changeMap.inorderTraversal((key, change) => {
- changes.push(change);
- });
- return changes;
- }
- }
- class ViewSnapshot {
- constructor(query, docs, oldDocs, docChanges, mutatedKeys, fromCache, syncStateChanged, excludesMetadataChanges, hasCachedResults) {
- this.query = query;
- this.docs = docs;
- this.oldDocs = oldDocs;
- this.docChanges = docChanges;
- this.mutatedKeys = mutatedKeys;
- this.fromCache = fromCache;
- this.syncStateChanged = syncStateChanged;
- this.excludesMetadataChanges = excludesMetadataChanges;
- this.hasCachedResults = hasCachedResults;
- }
- /** Returns a view snapshot as if all documents in the snapshot were added. */
- static fromInitialDocuments(query, documents, mutatedKeys, fromCache, hasCachedResults) {
- const changes = [];
- documents.forEach(doc => {
- changes.push({ type: 0 /* ChangeType.Added */, doc });
- });
- return new ViewSnapshot(query, documents, DocumentSet.emptySet(documents), changes, mutatedKeys, fromCache,
- /* syncStateChanged= */ true,
- /* excludesMetadataChanges= */ false, hasCachedResults);
- }
- get hasPendingWrites() {
- return !this.mutatedKeys.isEmpty();
- }
- isEqual(other) {
- if (this.fromCache !== other.fromCache ||
- this.hasCachedResults !== other.hasCachedResults ||
- this.syncStateChanged !== other.syncStateChanged ||
- !this.mutatedKeys.isEqual(other.mutatedKeys) ||
- !queryEquals(this.query, other.query) ||
- !this.docs.isEqual(other.docs) ||
- !this.oldDocs.isEqual(other.oldDocs)) {
- return false;
- }
- const changes = this.docChanges;
- const otherChanges = other.docChanges;
- if (changes.length !== otherChanges.length) {
- return false;
- }
- for (let i = 0; i < changes.length; i++) {
- if (changes[i].type !== otherChanges[i].type ||
- !changes[i].doc.isEqual(otherChanges[i].doc)) {
- return false;
- }
- }
- return true;
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Holds the listeners and the last received ViewSnapshot for a query being
- * tracked by EventManager.
- */
- class QueryListenersInfo {
- constructor() {
- this.viewSnap = undefined;
- this.listeners = [];
- }
- }
- function newEventManager() {
- return new EventManagerImpl();
- }
- class EventManagerImpl {
- constructor() {
- this.queries = new ObjectMap(q => canonifyQuery(q), queryEquals);
- this.onlineState = "Unknown" /* OnlineState.Unknown */;
- this.snapshotsInSyncListeners = new Set();
- }
- }
- async function eventManagerListen(eventManager, listener) {
- const eventManagerImpl = debugCast(eventManager);
- const query = listener.query;
- let firstListen = false;
- let queryInfo = eventManagerImpl.queries.get(query);
- if (!queryInfo) {
- firstListen = true;
- queryInfo = new QueryListenersInfo();
- }
- if (firstListen) {
- try {
- queryInfo.viewSnap = await eventManagerImpl.onListen(query);
- }
- catch (e) {
- const firestoreError = wrapInUserErrorIfRecoverable(e, `Initialization of query '${stringifyQuery(listener.query)}' failed`);
- listener.onError(firestoreError);
- return;
- }
- }
- eventManagerImpl.queries.set(query, queryInfo);
- queryInfo.listeners.push(listener);
- // Run global snapshot listeners if a consistent snapshot has been emitted.
- listener.applyOnlineStateChange(eventManagerImpl.onlineState);
- if (queryInfo.viewSnap) {
- const raisedEvent = listener.onViewSnapshot(queryInfo.viewSnap);
- if (raisedEvent) {
- raiseSnapshotsInSyncEvent(eventManagerImpl);
- }
- }
- }
- async function eventManagerUnlisten(eventManager, listener) {
- const eventManagerImpl = debugCast(eventManager);
- const query = listener.query;
- let lastListen = false;
- const queryInfo = eventManagerImpl.queries.get(query);
- if (queryInfo) {
- const i = queryInfo.listeners.indexOf(listener);
- if (i >= 0) {
- queryInfo.listeners.splice(i, 1);
- lastListen = queryInfo.listeners.length === 0;
- }
- }
- if (lastListen) {
- eventManagerImpl.queries.delete(query);
- return eventManagerImpl.onUnlisten(query);
- }
- }
- function eventManagerOnWatchChange(eventManager, viewSnaps) {
- const eventManagerImpl = debugCast(eventManager);
- let raisedEvent = false;
- for (const viewSnap of viewSnaps) {
- const query = viewSnap.query;
- const queryInfo = eventManagerImpl.queries.get(query);
- if (queryInfo) {
- for (const listener of queryInfo.listeners) {
- if (listener.onViewSnapshot(viewSnap)) {
- raisedEvent = true;
- }
- }
- queryInfo.viewSnap = viewSnap;
- }
- }
- if (raisedEvent) {
- raiseSnapshotsInSyncEvent(eventManagerImpl);
- }
- }
- function eventManagerOnWatchError(eventManager, query, error) {
- const eventManagerImpl = debugCast(eventManager);
- const queryInfo = eventManagerImpl.queries.get(query);
- if (queryInfo) {
- for (const listener of queryInfo.listeners) {
- listener.onError(error);
- }
- }
- // Remove all listeners. NOTE: We don't need to call syncEngine.unlisten()
- // after an error.
- eventManagerImpl.queries.delete(query);
- }
- function eventManagerOnOnlineStateChange(eventManager, onlineState) {
- const eventManagerImpl = debugCast(eventManager);
- eventManagerImpl.onlineState = onlineState;
- let raisedEvent = false;
- eventManagerImpl.queries.forEach((_, queryInfo) => {
- for (const listener of queryInfo.listeners) {
- // Run global snapshot listeners if a consistent snapshot has been emitted.
- if (listener.applyOnlineStateChange(onlineState)) {
- raisedEvent = true;
- }
- }
- });
- if (raisedEvent) {
- raiseSnapshotsInSyncEvent(eventManagerImpl);
- }
- }
- function addSnapshotsInSyncListener(eventManager, observer) {
- const eventManagerImpl = debugCast(eventManager);
- eventManagerImpl.snapshotsInSyncListeners.add(observer);
- // Immediately fire an initial event, indicating all existing listeners
- // are in-sync.
- observer.next();
- }
- function removeSnapshotsInSyncListener(eventManager, observer) {
- const eventManagerImpl = debugCast(eventManager);
- eventManagerImpl.snapshotsInSyncListeners.delete(observer);
- }
- // Call all global snapshot listeners that have been set.
- function raiseSnapshotsInSyncEvent(eventManagerImpl) {
- eventManagerImpl.snapshotsInSyncListeners.forEach(observer => {
- observer.next();
- });
- }
- /**
- * QueryListener takes a series of internal view snapshots and determines
- * when to raise the event.
- *
- * It uses an Observer to dispatch events.
- */
- class QueryListener {
- constructor(query, queryObserver, options) {
- this.query = query;
- this.queryObserver = queryObserver;
- /**
- * Initial snapshots (e.g. from cache) may not be propagated to the wrapped
- * observer. This flag is set to true once we've actually raised an event.
- */
- this.raisedInitialEvent = false;
- this.snap = null;
- this.onlineState = "Unknown" /* OnlineState.Unknown */;
- this.options = options || {};
- }
- /**
- * Applies the new ViewSnapshot to this listener, raising a user-facing event
- * if applicable (depending on what changed, whether the user has opted into
- * metadata-only changes, etc.). Returns true if a user-facing event was
- * indeed raised.
- */
- onViewSnapshot(snap) {
- if (!this.options.includeMetadataChanges) {
- // Remove the metadata only changes.
- const docChanges = [];
- for (const docChange of snap.docChanges) {
- if (docChange.type !== 3 /* ChangeType.Metadata */) {
- docChanges.push(docChange);
- }
- }
- snap = new ViewSnapshot(snap.query, snap.docs, snap.oldDocs, docChanges, snap.mutatedKeys, snap.fromCache, snap.syncStateChanged,
- /* excludesMetadataChanges= */ true, snap.hasCachedResults);
- }
- let raisedEvent = false;
- if (!this.raisedInitialEvent) {
- if (this.shouldRaiseInitialEvent(snap, this.onlineState)) {
- this.raiseInitialEvent(snap);
- raisedEvent = true;
- }
- }
- else if (this.shouldRaiseEvent(snap)) {
- this.queryObserver.next(snap);
- raisedEvent = true;
- }
- this.snap = snap;
- return raisedEvent;
- }
- onError(error) {
- this.queryObserver.error(error);
- }
- /** Returns whether a snapshot was raised. */
- applyOnlineStateChange(onlineState) {
- this.onlineState = onlineState;
- let raisedEvent = false;
- if (this.snap &&
- !this.raisedInitialEvent &&
- this.shouldRaiseInitialEvent(this.snap, onlineState)) {
- this.raiseInitialEvent(this.snap);
- raisedEvent = true;
- }
- return raisedEvent;
- }
- shouldRaiseInitialEvent(snap, onlineState) {
- // Always raise the first event when we're synced
- if (!snap.fromCache) {
- return true;
- }
- // NOTE: We consider OnlineState.Unknown as online (it should become Offline
- // or Online if we wait long enough).
- const maybeOnline = onlineState !== "Offline" /* OnlineState.Offline */;
- // Don't raise the event if we're online, aren't synced yet (checked
- // above) and are waiting for a sync.
- if (this.options.waitForSyncWhenOnline && maybeOnline) {
- return false;
- }
- // Raise data from cache if we have any documents, have cached results before,
- // or we are offline.
- return (!snap.docs.isEmpty() ||
- snap.hasCachedResults ||
- onlineState === "Offline" /* OnlineState.Offline */);
- }
- shouldRaiseEvent(snap) {
- // We don't need to handle includeDocumentMetadataChanges here because
- // the Metadata only changes have already been stripped out if needed.
- // At this point the only changes we will see are the ones we should
- // propagate.
- if (snap.docChanges.length > 0) {
- return true;
- }
- const hasPendingWritesChanged = this.snap && this.snap.hasPendingWrites !== snap.hasPendingWrites;
- if (snap.syncStateChanged || hasPendingWritesChanged) {
- return this.options.includeMetadataChanges === true;
- }
- // Generally we should have hit one of the cases above, but it's possible
- // to get here if there were only metadata docChanges and they got
- // stripped out.
- return false;
- }
- raiseInitialEvent(snap) {
- snap = ViewSnapshot.fromInitialDocuments(snap.query, snap.docs, snap.mutatedKeys, snap.fromCache, snap.hasCachedResults);
- this.raisedInitialEvent = true;
- this.queryObserver.next(snap);
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * A set of changes to what documents are currently in view and out of view for
- * a given query. These changes are sent to the LocalStore by the View (via
- * the SyncEngine) and are used to pin / unpin documents as appropriate.
- */
- class LocalViewChanges {
- constructor(targetId, fromCache, addedKeys, removedKeys) {
- this.targetId = targetId;
- this.fromCache = fromCache;
- this.addedKeys = addedKeys;
- this.removedKeys = removedKeys;
- }
- static fromSnapshot(targetId, viewSnapshot) {
- let addedKeys = documentKeySet();
- let removedKeys = documentKeySet();
- for (const docChange of viewSnapshot.docChanges) {
- switch (docChange.type) {
- case 0 /* ChangeType.Added */:
- addedKeys = addedKeys.add(docChange.doc.key);
- break;
- case 1 /* ChangeType.Removed */:
- removedKeys = removedKeys.add(docChange.doc.key);
- break;
- // do nothing
- }
- }
- return new LocalViewChanges(targetId, viewSnapshot.fromCache, addedKeys, removedKeys);
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Helper to convert objects from bundles to model objects in the SDK.
- */
- class BundleConverterImpl {
- constructor(serializer) {
- this.serializer = serializer;
- }
- toDocumentKey(name) {
- return fromName(this.serializer, name);
- }
- /**
- * Converts a BundleDocument to a MutableDocument.
- */
- toMutableDocument(bundledDoc) {
- if (bundledDoc.metadata.exists) {
- return fromDocument(this.serializer, bundledDoc.document, false);
- }
- else {
- return MutableDocument.newNoDocument(this.toDocumentKey(bundledDoc.metadata.name), this.toSnapshotVersion(bundledDoc.metadata.readTime));
- }
- }
- toSnapshotVersion(time) {
- return fromVersion(time);
- }
- }
- /**
- * A class to process the elements from a bundle, load them into local
- * storage and provide progress update while loading.
- */
- class BundleLoader {
- constructor(bundleMetadata, localStore, serializer) {
- this.bundleMetadata = bundleMetadata;
- this.localStore = localStore;
- this.serializer = serializer;
- /** Batched queries to be saved into storage */
- this.queries = [];
- /** Batched documents to be saved into storage */
- this.documents = [];
- /** The collection groups affected by this bundle. */
- this.collectionGroups = new Set();
- this.progress = bundleInitialProgress(bundleMetadata);
- }
- /**
- * Adds an element from the bundle to the loader.
- *
- * Returns a new progress if adding the element leads to a new progress,
- * otherwise returns null.
- */
- addSizedElement(element) {
- this.progress.bytesLoaded += element.byteLength;
- let documentsLoaded = this.progress.documentsLoaded;
- if (element.payload.namedQuery) {
- this.queries.push(element.payload.namedQuery);
- }
- else if (element.payload.documentMetadata) {
- this.documents.push({ metadata: element.payload.documentMetadata });
- if (!element.payload.documentMetadata.exists) {
- ++documentsLoaded;
- }
- const path = ResourcePath.fromString(element.payload.documentMetadata.name);
- this.collectionGroups.add(path.get(path.length - 2));
- }
- else if (element.payload.document) {
- this.documents[this.documents.length - 1].document =
- element.payload.document;
- ++documentsLoaded;
- }
- if (documentsLoaded !== this.progress.documentsLoaded) {
- this.progress.documentsLoaded = documentsLoaded;
- return Object.assign({}, this.progress);
- }
- return null;
- }
- getQueryDocumentMapping(documents) {
- const queryDocumentMap = new Map();
- const bundleConverter = new BundleConverterImpl(this.serializer);
- for (const bundleDoc of documents) {
- if (bundleDoc.metadata.queries) {
- const documentKey = bundleConverter.toDocumentKey(bundleDoc.metadata.name);
- for (const queryName of bundleDoc.metadata.queries) {
- const documentKeys = (queryDocumentMap.get(queryName) || documentKeySet()).add(documentKey);
- queryDocumentMap.set(queryName, documentKeys);
- }
- }
- }
- return queryDocumentMap;
- }
- /**
- * Update the progress to 'Success' and return the updated progress.
- */
- async complete() {
- const changedDocs = await localStoreApplyBundledDocuments(this.localStore, new BundleConverterImpl(this.serializer), this.documents, this.bundleMetadata.id);
- const queryDocumentMap = this.getQueryDocumentMapping(this.documents);
- for (const q of this.queries) {
- await localStoreSaveNamedQuery(this.localStore, q, queryDocumentMap.get(q.name));
- }
- this.progress.taskState = 'Success';
- return {
- progress: this.progress,
- changedCollectionGroups: this.collectionGroups,
- changedDocs
- };
- }
- }
- /**
- * Returns a `LoadBundleTaskProgress` representing the initial progress of
- * loading a bundle.
- */
- function bundleInitialProgress(metadata) {
- return {
- taskState: 'Running',
- documentsLoaded: 0,
- bytesLoaded: 0,
- totalDocuments: metadata.totalDocuments,
- totalBytes: metadata.totalBytes
- };
- }
- /**
- * Returns a `LoadBundleTaskProgress` representing the progress that the loading
- * has succeeded.
- */
- function bundleSuccessProgress(metadata) {
- return {
- taskState: 'Success',
- documentsLoaded: metadata.totalDocuments,
- bytesLoaded: metadata.totalBytes,
- totalDocuments: metadata.totalDocuments,
- totalBytes: metadata.totalBytes
- };
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- class AddedLimboDocument {
- constructor(key) {
- this.key = key;
- }
- }
- class RemovedLimboDocument {
- constructor(key) {
- this.key = key;
- }
- }
- /**
- * View is responsible for computing the final merged truth of what docs are in
- * a query. It gets notified of local and remote changes to docs, and applies
- * the query filters and limits to determine the most correct possible results.
- */
- class View {
- constructor(query,
- /** Documents included in the remote target */
- _syncedDocuments) {
- this.query = query;
- this._syncedDocuments = _syncedDocuments;
- this.syncState = null;
- this.hasCachedResults = false;
- /**
- * A flag whether the view is current with the backend. A view is considered
- * current after it has seen the current flag from the backend and did not
- * lose consistency within the watch stream (e.g. because of an existence
- * filter mismatch).
- */
- this.current = false;
- /** Documents in the view but not in the remote target */
- this.limboDocuments = documentKeySet();
- /** Document Keys that have local changes */
- this.mutatedKeys = documentKeySet();
- this.docComparator = newQueryComparator(query);
- this.documentSet = new DocumentSet(this.docComparator);
- }
- /**
- * The set of remote documents that the server has told us belongs to the target associated with
- * this view.
- */
- get syncedDocuments() {
- return this._syncedDocuments;
- }
- /**
- * Iterates over a set of doc changes, applies the query limit, and computes
- * what the new results should be, what the changes were, and whether we may
- * need to go back to the local cache for more results. Does not make any
- * changes to the view.
- * @param docChanges - The doc changes to apply to this view.
- * @param previousChanges - If this is being called with a refill, then start
- * with this set of docs and changes instead of the current view.
- * @returns a new set of docs, changes, and refill flag.
- */
- computeDocChanges(docChanges, previousChanges) {
- const changeSet = previousChanges
- ? previousChanges.changeSet
- : new DocumentChangeSet();
- const oldDocumentSet = previousChanges
- ? previousChanges.documentSet
- : this.documentSet;
- let newMutatedKeys = previousChanges
- ? previousChanges.mutatedKeys
- : this.mutatedKeys;
- let newDocumentSet = oldDocumentSet;
- let needsRefill = false;
- // Track the last doc in a (full) limit. This is necessary, because some
- // update (a delete, or an update moving a doc past the old limit) might
- // mean there is some other document in the local cache that either should
- // come (1) between the old last limit doc and the new last document, in the
- // case of updates, or (2) after the new last document, in the case of
- // deletes. So we keep this doc at the old limit to compare the updates to.
- //
- // Note that this should never get used in a refill (when previousChanges is
- // set), because there will only be adds -- no deletes or updates.
- const lastDocInLimit = this.query.limitType === "F" /* LimitType.First */ &&
- oldDocumentSet.size === this.query.limit
- ? oldDocumentSet.last()
- : null;
- const firstDocInLimit = this.query.limitType === "L" /* LimitType.Last */ &&
- oldDocumentSet.size === this.query.limit
- ? oldDocumentSet.first()
- : null;
- docChanges.inorderTraversal((key, entry) => {
- const oldDoc = oldDocumentSet.get(key);
- const newDoc = queryMatches(this.query, entry) ? entry : null;
- const oldDocHadPendingMutations = oldDoc
- ? this.mutatedKeys.has(oldDoc.key)
- : false;
- const newDocHasPendingMutations = newDoc
- ? newDoc.hasLocalMutations ||
- // We only consider committed mutations for documents that were
- // mutated during the lifetime of the view.
- (this.mutatedKeys.has(newDoc.key) && newDoc.hasCommittedMutations)
- : false;
- let changeApplied = false;
- // Calculate change
- if (oldDoc && newDoc) {
- const docsEqual = oldDoc.data.isEqual(newDoc.data);
- if (!docsEqual) {
- if (!this.shouldWaitForSyncedDocument(oldDoc, newDoc)) {
- changeSet.track({
- type: 2 /* ChangeType.Modified */,
- doc: newDoc
- });
- changeApplied = true;
- if ((lastDocInLimit &&
- this.docComparator(newDoc, lastDocInLimit) > 0) ||
- (firstDocInLimit &&
- this.docComparator(newDoc, firstDocInLimit) < 0)) {
- // This doc moved from inside the limit to outside the limit.
- // That means there may be some other doc in the local cache
- // that should be included instead.
- needsRefill = true;
- }
- }
- }
- else if (oldDocHadPendingMutations !== newDocHasPendingMutations) {
- changeSet.track({ type: 3 /* ChangeType.Metadata */, doc: newDoc });
- changeApplied = true;
- }
- }
- else if (!oldDoc && newDoc) {
- changeSet.track({ type: 0 /* ChangeType.Added */, doc: newDoc });
- changeApplied = true;
- }
- else if (oldDoc && !newDoc) {
- changeSet.track({ type: 1 /* ChangeType.Removed */, doc: oldDoc });
- changeApplied = true;
- if (lastDocInLimit || firstDocInLimit) {
- // A doc was removed from a full limit query. We'll need to
- // requery from the local cache to see if we know about some other
- // doc that should be in the results.
- needsRefill = true;
- }
- }
- if (changeApplied) {
- if (newDoc) {
- newDocumentSet = newDocumentSet.add(newDoc);
- if (newDocHasPendingMutations) {
- newMutatedKeys = newMutatedKeys.add(key);
- }
- else {
- newMutatedKeys = newMutatedKeys.delete(key);
- }
- }
- else {
- newDocumentSet = newDocumentSet.delete(key);
- newMutatedKeys = newMutatedKeys.delete(key);
- }
- }
- });
- // Drop documents out to meet limit/limitToLast requirement.
- if (this.query.limit !== null) {
- while (newDocumentSet.size > this.query.limit) {
- const oldDoc = this.query.limitType === "F" /* LimitType.First */
- ? newDocumentSet.last()
- : newDocumentSet.first();
- newDocumentSet = newDocumentSet.delete(oldDoc.key);
- newMutatedKeys = newMutatedKeys.delete(oldDoc.key);
- changeSet.track({ type: 1 /* ChangeType.Removed */, doc: oldDoc });
- }
- }
- return {
- documentSet: newDocumentSet,
- changeSet,
- needsRefill,
- mutatedKeys: newMutatedKeys
- };
- }
- shouldWaitForSyncedDocument(oldDoc, newDoc) {
- // We suppress the initial change event for documents that were modified as
- // part of a write acknowledgment (e.g. when the value of a server transform
- // is applied) as Watch will send us the same document again.
- // By suppressing the event, we only raise two user visible events (one with
- // `hasPendingWrites` and the final state of the document) instead of three
- // (one with `hasPendingWrites`, the modified document with
- // `hasPendingWrites` and the final state of the document).
- return (oldDoc.hasLocalMutations &&
- newDoc.hasCommittedMutations &&
- !newDoc.hasLocalMutations);
- }
- /**
- * Updates the view with the given ViewDocumentChanges and optionally updates
- * limbo docs and sync state from the provided target change.
- * @param docChanges - The set of changes to make to the view's docs.
- * @param updateLimboDocuments - Whether to update limbo documents based on
- * this change.
- * @param targetChange - A target change to apply for computing limbo docs and
- * sync state.
- * @returns A new ViewChange with the given docs, changes, and sync state.
- */
- // PORTING NOTE: The iOS/Android clients always compute limbo document changes.
- applyChanges(docChanges, updateLimboDocuments, targetChange) {
- const oldDocs = this.documentSet;
- this.documentSet = docChanges.documentSet;
- this.mutatedKeys = docChanges.mutatedKeys;
- // Sort changes based on type and query comparator
- const changes = docChanges.changeSet.getChanges();
- changes.sort((c1, c2) => {
- return (compareChangeType(c1.type, c2.type) ||
- this.docComparator(c1.doc, c2.doc));
- });
- this.applyTargetChange(targetChange);
- const limboChanges = updateLimboDocuments
- ? this.updateLimboDocuments()
- : [];
- const synced = this.limboDocuments.size === 0 && this.current;
- const newSyncState = synced ? 1 /* SyncState.Synced */ : 0 /* SyncState.Local */;
- const syncStateChanged = newSyncState !== this.syncState;
- this.syncState = newSyncState;
- if (changes.length === 0 && !syncStateChanged) {
- // no changes
- return { limboChanges };
- }
- else {
- const snap = new ViewSnapshot(this.query, docChanges.documentSet, oldDocs, changes, docChanges.mutatedKeys, newSyncState === 0 /* SyncState.Local */, syncStateChanged,
- /* excludesMetadataChanges= */ false, targetChange
- ? targetChange.resumeToken.approximateByteSize() > 0
- : false);
- return {
- snapshot: snap,
- limboChanges
- };
- }
- }
- /**
- * Applies an OnlineState change to the view, potentially generating a
- * ViewChange if the view's syncState changes as a result.
- */
- applyOnlineStateChange(onlineState) {
- if (this.current && onlineState === "Offline" /* OnlineState.Offline */) {
- // If we're offline, set `current` to false and then call applyChanges()
- // to refresh our syncState and generate a ViewChange as appropriate. We
- // are guaranteed to get a new TargetChange that sets `current` back to
- // true once the client is back online.
- this.current = false;
- return this.applyChanges({
- documentSet: this.documentSet,
- changeSet: new DocumentChangeSet(),
- mutatedKeys: this.mutatedKeys,
- needsRefill: false
- },
- /* updateLimboDocuments= */ false);
- }
- else {
- // No effect, just return a no-op ViewChange.
- return { limboChanges: [] };
- }
- }
- /**
- * Returns whether the doc for the given key should be in limbo.
- */
- shouldBeInLimbo(key) {
- // If the remote end says it's part of this query, it's not in limbo.
- if (this._syncedDocuments.has(key)) {
- return false;
- }
- // The local store doesn't think it's a result, so it shouldn't be in limbo.
- if (!this.documentSet.has(key)) {
- return false;
- }
- // If there are local changes to the doc, they might explain why the server
- // doesn't know that it's part of the query. So don't put it in limbo.
- // TODO(klimt): Ideally, we would only consider changes that might actually
- // affect this specific query.
- if (this.documentSet.get(key).hasLocalMutations) {
- return false;
- }
- // Everything else is in limbo.
- return true;
- }
- /**
- * Updates syncedDocuments, current, and limbo docs based on the given change.
- * Returns the list of changes to which docs are in limbo.
- */
- applyTargetChange(targetChange) {
- if (targetChange) {
- targetChange.addedDocuments.forEach(key => (this._syncedDocuments = this._syncedDocuments.add(key)));
- targetChange.modifiedDocuments.forEach(key => {
- });
- targetChange.removedDocuments.forEach(key => (this._syncedDocuments = this._syncedDocuments.delete(key)));
- this.current = targetChange.current;
- }
- }
- updateLimboDocuments() {
- // We can only determine limbo documents when we're in-sync with the server.
- if (!this.current) {
- return [];
- }
- // TODO(klimt): Do this incrementally so that it's not quadratic when
- // updating many documents.
- const oldLimboDocuments = this.limboDocuments;
- this.limboDocuments = documentKeySet();
- this.documentSet.forEach(doc => {
- if (this.shouldBeInLimbo(doc.key)) {
- this.limboDocuments = this.limboDocuments.add(doc.key);
- }
- });
- // Diff the new limbo docs with the old limbo docs.
- const changes = [];
- oldLimboDocuments.forEach(key => {
- if (!this.limboDocuments.has(key)) {
- changes.push(new RemovedLimboDocument(key));
- }
- });
- this.limboDocuments.forEach(key => {
- if (!oldLimboDocuments.has(key)) {
- changes.push(new AddedLimboDocument(key));
- }
- });
- return changes;
- }
- /**
- * Update the in-memory state of the current view with the state read from
- * persistence.
- *
- * We update the query view whenever a client's primary status changes:
- * - When a client transitions from primary to secondary, it can miss
- * LocalStorage updates and its query views may temporarily not be
- * synchronized with the state on disk.
- * - For secondary to primary transitions, the client needs to update the list
- * of `syncedDocuments` since secondary clients update their query views
- * based purely on synthesized RemoteEvents.
- *
- * @param queryResult.documents - The documents that match the query according
- * to the LocalStore.
- * @param queryResult.remoteKeys - The keys of the documents that match the
- * query according to the backend.
- *
- * @returns The ViewChange that resulted from this synchronization.
- */
- // PORTING NOTE: Multi-tab only.
- synchronizeWithPersistedState(queryResult) {
- this._syncedDocuments = queryResult.remoteKeys;
- this.limboDocuments = documentKeySet();
- const docChanges = this.computeDocChanges(queryResult.documents);
- return this.applyChanges(docChanges, /*updateLimboDocuments=*/ true);
- }
- /**
- * Returns a view snapshot as if this query was just listened to. Contains
- * a document add for every existing document and the `fromCache` and
- * `hasPendingWrites` status of the already established view.
- */
- // PORTING NOTE: Multi-tab only.
- computeInitialSnapshot() {
- return ViewSnapshot.fromInitialDocuments(this.query, this.documentSet, this.mutatedKeys, this.syncState === 0 /* SyncState.Local */, this.hasCachedResults);
- }
- }
- function compareChangeType(c1, c2) {
- const order = (change) => {
- switch (change) {
- case 0 /* ChangeType.Added */:
- return 1;
- case 2 /* ChangeType.Modified */:
- return 2;
- case 3 /* ChangeType.Metadata */:
- // A metadata change is converted to a modified change at the public
- // api layer. Since we sort by document key and then change type,
- // metadata and modified changes must be sorted equivalently.
- return 2;
- case 1 /* ChangeType.Removed */:
- return 0;
- default:
- return fail();
- }
- };
- return order(c1) - order(c2);
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const LOG_TAG$3 = 'SyncEngine';
- /**
- * QueryView contains all of the data that SyncEngine needs to keep track of for
- * a particular query.
- */
- class QueryView {
- constructor(
- /**
- * The query itself.
- */
- query,
- /**
- * The target number created by the client that is used in the watch
- * stream to identify this query.
- */
- targetId,
- /**
- * The view is responsible for computing the final merged truth of what
- * docs are in the query. It gets notified of local and remote changes,
- * and applies the query filters and limits to determine the most correct
- * possible results.
- */
- view) {
- this.query = query;
- this.targetId = targetId;
- this.view = view;
- }
- }
- /** Tracks a limbo resolution. */
- class LimboResolution {
- constructor(key) {
- this.key = key;
- /**
- * Set to true once we've received a document. This is used in
- * getRemoteKeysForTarget() and ultimately used by WatchChangeAggregator to
- * decide whether it needs to manufacture a delete event for the target once
- * the target is CURRENT.
- */
- this.receivedDocument = false;
- }
- }
- /**
- * An implementation of `SyncEngine` coordinating with other parts of SDK.
- *
- * The parts of SyncEngine that act as a callback to RemoteStore need to be
- * registered individually. This is done in `syncEngineWrite()` and
- * `syncEngineListen()` (as well as `applyPrimaryState()`) as these methods
- * serve as entry points to RemoteStore's functionality.
- *
- * Note: some field defined in this class might have public access level, but
- * the class is not exported so they are only accessible from this module.
- * This is useful to implement optional features (like bundles) in free
- * functions, such that they are tree-shakeable.
- */
- class SyncEngineImpl {
- constructor(localStore, remoteStore, eventManager,
- // PORTING NOTE: Manages state synchronization in multi-tab environments.
- sharedClientState, currentUser, maxConcurrentLimboResolutions) {
- this.localStore = localStore;
- this.remoteStore = remoteStore;
- this.eventManager = eventManager;
- this.sharedClientState = sharedClientState;
- this.currentUser = currentUser;
- this.maxConcurrentLimboResolutions = maxConcurrentLimboResolutions;
- this.syncEngineListener = {};
- this.queryViewsByQuery = new ObjectMap(q => canonifyQuery(q), queryEquals);
- this.queriesByTarget = new Map();
- /**
- * The keys of documents that are in limbo for which we haven't yet started a
- * limbo resolution query. The strings in this set are the result of calling
- * `key.path.canonicalString()` where `key` is a `DocumentKey` object.
- *
- * The `Set` type was chosen because it provides efficient lookup and removal
- * of arbitrary elements and it also maintains insertion order, providing the
- * desired queue-like FIFO semantics.
- */
- this.enqueuedLimboResolutions = new Set();
- /**
- * Keeps track of the target ID for each document that is in limbo with an
- * active target.
- */
- this.activeLimboTargetsByKey = new SortedMap(DocumentKey.comparator);
- /**
- * Keeps track of the information about an active limbo resolution for each
- * active target ID that was started for the purpose of limbo resolution.
- */
- this.activeLimboResolutionsByTarget = new Map();
- this.limboDocumentRefs = new ReferenceSet();
- /** Stores user completion handlers, indexed by User and BatchId. */
- this.mutationUserCallbacks = {};
- /** Stores user callbacks waiting for all pending writes to be acknowledged. */
- this.pendingWritesCallbacks = new Map();
- this.limboTargetIdGenerator = TargetIdGenerator.forSyncEngine();
- this.onlineState = "Unknown" /* OnlineState.Unknown */;
- // The primary state is set to `true` or `false` immediately after Firestore
- // startup. In the interim, a client should only be considered primary if
- // `isPrimary` is true.
- this._isPrimaryClient = undefined;
- }
- get isPrimaryClient() {
- return this._isPrimaryClient === true;
- }
- }
- function newSyncEngine(localStore, remoteStore, eventManager,
- // PORTING NOTE: Manages state synchronization in multi-tab environments.
- sharedClientState, currentUser, maxConcurrentLimboResolutions, isPrimary) {
- const syncEngine = new SyncEngineImpl(localStore, remoteStore, eventManager, sharedClientState, currentUser, maxConcurrentLimboResolutions);
- if (isPrimary) {
- syncEngine._isPrimaryClient = true;
- }
- return syncEngine;
- }
- /**
- * Initiates the new listen, resolves promise when listen enqueued to the
- * server. All the subsequent view snapshots or errors are sent to the
- * subscribed handlers. Returns the initial snapshot.
- */
- async function syncEngineListen(syncEngine, query) {
- const syncEngineImpl = ensureWatchCallbacks(syncEngine);
- let targetId;
- let viewSnapshot;
- const queryView = syncEngineImpl.queryViewsByQuery.get(query);
- if (queryView) {
- // PORTING NOTE: With Multi-Tab Web, it is possible that a query view
- // already exists when EventManager calls us for the first time. This
- // happens when the primary tab is already listening to this query on
- // behalf of another tab and the user of the primary also starts listening
- // to the query. EventManager will not have an assigned target ID in this
- // case and calls `listen` to obtain this ID.
- targetId = queryView.targetId;
- syncEngineImpl.sharedClientState.addLocalQueryTarget(targetId);
- viewSnapshot = queryView.view.computeInitialSnapshot();
- }
- else {
- const targetData = await localStoreAllocateTarget(syncEngineImpl.localStore, queryToTarget(query));
- const status = syncEngineImpl.sharedClientState.addLocalQueryTarget(targetData.targetId);
- targetId = targetData.targetId;
- viewSnapshot = await initializeViewAndComputeSnapshot(syncEngineImpl, query, targetId, status === 'current', targetData.resumeToken);
- if (syncEngineImpl.isPrimaryClient) {
- remoteStoreListen(syncEngineImpl.remoteStore, targetData);
- }
- }
- return viewSnapshot;
- }
- /**
- * Registers a view for a previously unknown query and computes its initial
- * snapshot.
- */
- async function initializeViewAndComputeSnapshot(syncEngineImpl, query, targetId, current, resumeToken) {
- // PORTING NOTE: On Web only, we inject the code that registers new Limbo
- // targets based on view changes. This allows us to only depend on Limbo
- // changes when user code includes queries.
- syncEngineImpl.applyDocChanges = (queryView, changes, remoteEvent) => applyDocChanges(syncEngineImpl, queryView, changes, remoteEvent);
- const queryResult = await localStoreExecuteQuery(syncEngineImpl.localStore, query,
- /* usePreviousResults= */ true);
- const view = new View(query, queryResult.remoteKeys);
- const viewDocChanges = view.computeDocChanges(queryResult.documents);
- const synthesizedTargetChange = TargetChange.createSynthesizedTargetChangeForCurrentChange(targetId, current && syncEngineImpl.onlineState !== "Offline" /* OnlineState.Offline */, resumeToken);
- const viewChange = view.applyChanges(viewDocChanges,
- /* updateLimboDocuments= */ syncEngineImpl.isPrimaryClient, synthesizedTargetChange);
- updateTrackedLimbos(syncEngineImpl, targetId, viewChange.limboChanges);
- const data = new QueryView(query, targetId, view);
- syncEngineImpl.queryViewsByQuery.set(query, data);
- if (syncEngineImpl.queriesByTarget.has(targetId)) {
- syncEngineImpl.queriesByTarget.get(targetId).push(query);
- }
- else {
- syncEngineImpl.queriesByTarget.set(targetId, [query]);
- }
- return viewChange.snapshot;
- }
- /** Stops listening to the query. */
- async function syncEngineUnlisten(syncEngine, query) {
- const syncEngineImpl = debugCast(syncEngine);
- const queryView = syncEngineImpl.queryViewsByQuery.get(query);
- // Only clean up the query view and target if this is the only query mapped
- // to the target.
- const queries = syncEngineImpl.queriesByTarget.get(queryView.targetId);
- if (queries.length > 1) {
- syncEngineImpl.queriesByTarget.set(queryView.targetId, queries.filter(q => !queryEquals(q, query)));
- syncEngineImpl.queryViewsByQuery.delete(query);
- return;
- }
- // No other queries are mapped to the target, clean up the query and the target.
- if (syncEngineImpl.isPrimaryClient) {
- // We need to remove the local query target first to allow us to verify
- // whether any other client is still interested in this target.
- syncEngineImpl.sharedClientState.removeLocalQueryTarget(queryView.targetId);
- const targetRemainsActive = syncEngineImpl.sharedClientState.isActiveQueryTarget(queryView.targetId);
- if (!targetRemainsActive) {
- await localStoreReleaseTarget(syncEngineImpl.localStore, queryView.targetId,
- /*keepPersistedTargetData=*/ false)
- .then(() => {
- syncEngineImpl.sharedClientState.clearQueryState(queryView.targetId);
- remoteStoreUnlisten(syncEngineImpl.remoteStore, queryView.targetId);
- removeAndCleanupTarget(syncEngineImpl, queryView.targetId);
- })
- .catch(ignoreIfPrimaryLeaseLoss);
- }
- }
- else {
- removeAndCleanupTarget(syncEngineImpl, queryView.targetId);
- await localStoreReleaseTarget(syncEngineImpl.localStore, queryView.targetId,
- /*keepPersistedTargetData=*/ true);
- }
- }
- /**
- * Initiates the write of local mutation batch which involves adding the
- * writes to the mutation queue, notifying the remote store about new
- * mutations and raising events for any changes this write caused.
- *
- * The promise returned by this call is resolved when the above steps
- * have completed, *not* when the write was acked by the backend. The
- * userCallback is resolved once the write was acked/rejected by the
- * backend (or failed locally for any other reason).
- */
- async function syncEngineWrite(syncEngine, batch, userCallback) {
- const syncEngineImpl = syncEngineEnsureWriteCallbacks(syncEngine);
- try {
- const result = await localStoreWriteLocally(syncEngineImpl.localStore, batch);
- syncEngineImpl.sharedClientState.addPendingMutation(result.batchId);
- addMutationCallback(syncEngineImpl, result.batchId, userCallback);
- await syncEngineEmitNewSnapsAndNotifyLocalStore(syncEngineImpl, result.changes);
- await fillWritePipeline(syncEngineImpl.remoteStore);
- }
- catch (e) {
- // If we can't persist the mutation, we reject the user callback and
- // don't send the mutation. The user can then retry the write.
- const error = wrapInUserErrorIfRecoverable(e, `Failed to persist write`);
- userCallback.reject(error);
- }
- }
- /**
- * Applies one remote event to the sync engine, notifying any views of the
- * changes, and releasing any pending mutation batches that would become
- * visible because of the snapshot version the remote event contains.
- */
- async function syncEngineApplyRemoteEvent(syncEngine, remoteEvent) {
- const syncEngineImpl = debugCast(syncEngine);
- try {
- const changes = await localStoreApplyRemoteEventToLocalCache(syncEngineImpl.localStore, remoteEvent);
- // Update `receivedDocument` as appropriate for any limbo targets.
- remoteEvent.targetChanges.forEach((targetChange, targetId) => {
- const limboResolution = syncEngineImpl.activeLimboResolutionsByTarget.get(targetId);
- if (limboResolution) {
- // Since this is a limbo resolution lookup, it's for a single document
- // and it could be added, modified, or removed, but not a combination.
- hardAssert(targetChange.addedDocuments.size +
- targetChange.modifiedDocuments.size +
- targetChange.removedDocuments.size <=
- 1);
- if (targetChange.addedDocuments.size > 0) {
- limboResolution.receivedDocument = true;
- }
- else if (targetChange.modifiedDocuments.size > 0) {
- hardAssert(limboResolution.receivedDocument);
- }
- else if (targetChange.removedDocuments.size > 0) {
- hardAssert(limboResolution.receivedDocument);
- limboResolution.receivedDocument = false;
- }
- else {
- // This was probably just a CURRENT targetChange or similar.
- }
- }
- });
- await syncEngineEmitNewSnapsAndNotifyLocalStore(syncEngineImpl, changes, remoteEvent);
- }
- catch (error) {
- await ignoreIfPrimaryLeaseLoss(error);
- }
- }
- /**
- * Applies an OnlineState change to the sync engine and notifies any views of
- * the change.
- */
- function syncEngineApplyOnlineStateChange(syncEngine, onlineState, source) {
- const syncEngineImpl = debugCast(syncEngine);
- // If we are the secondary client, we explicitly ignore the remote store's
- // online state (the local client may go offline, even though the primary
- // tab remains online) and only apply the primary tab's online state from
- // SharedClientState.
- if ((syncEngineImpl.isPrimaryClient &&
- source === 0 /* OnlineStateSource.RemoteStore */) ||
- (!syncEngineImpl.isPrimaryClient &&
- source === 1 /* OnlineStateSource.SharedClientState */)) {
- const newViewSnapshots = [];
- syncEngineImpl.queryViewsByQuery.forEach((query, queryView) => {
- const viewChange = queryView.view.applyOnlineStateChange(onlineState);
- if (viewChange.snapshot) {
- newViewSnapshots.push(viewChange.snapshot);
- }
- });
- eventManagerOnOnlineStateChange(syncEngineImpl.eventManager, onlineState);
- if (newViewSnapshots.length) {
- syncEngineImpl.syncEngineListener.onWatchChange(newViewSnapshots);
- }
- syncEngineImpl.onlineState = onlineState;
- if (syncEngineImpl.isPrimaryClient) {
- syncEngineImpl.sharedClientState.setOnlineState(onlineState);
- }
- }
- }
- /**
- * Rejects the listen for the given targetID. This can be triggered by the
- * backend for any active target.
- *
- * @param syncEngine - The sync engine implementation.
- * @param targetId - The targetID corresponds to one previously initiated by the
- * user as part of TargetData passed to listen() on RemoteStore.
- * @param err - A description of the condition that has forced the rejection.
- * Nearly always this will be an indication that the user is no longer
- * authorized to see the data matching the target.
- */
- async function syncEngineRejectListen(syncEngine, targetId, err) {
- const syncEngineImpl = debugCast(syncEngine);
- // PORTING NOTE: Multi-tab only.
- syncEngineImpl.sharedClientState.updateQueryState(targetId, 'rejected', err);
- const limboResolution = syncEngineImpl.activeLimboResolutionsByTarget.get(targetId);
- const limboKey = limboResolution && limboResolution.key;
- if (limboKey) {
- // TODO(klimt): We really only should do the following on permission
- // denied errors, but we don't have the cause code here.
- // It's a limbo doc. Create a synthetic event saying it was deleted.
- // This is kind of a hack. Ideally, we would have a method in the local
- // store to purge a document. However, it would be tricky to keep all of
- // the local store's invariants with another method.
- let documentUpdates = new SortedMap(DocumentKey.comparator);
- // TODO(b/217189216): This limbo document should ideally have a read time,
- // so that it is picked up by any read-time based scans. The backend,
- // however, does not send a read time for target removals.
- documentUpdates = documentUpdates.insert(limboKey, MutableDocument.newNoDocument(limboKey, SnapshotVersion.min()));
- const resolvedLimboDocuments = documentKeySet().add(limboKey);
- const event = new RemoteEvent(SnapshotVersion.min(),
- /* targetChanges= */ new Map(),
- /* targetMismatches= */ new SortedMap(primitiveComparator), documentUpdates, resolvedLimboDocuments);
- await syncEngineApplyRemoteEvent(syncEngineImpl, event);
- // Since this query failed, we won't want to manually unlisten to it.
- // We only remove it from bookkeeping after we successfully applied the
- // RemoteEvent. If `applyRemoteEvent()` throws, we want to re-listen to
- // this query when the RemoteStore restarts the Watch stream, which should
- // re-trigger the target failure.
- syncEngineImpl.activeLimboTargetsByKey =
- syncEngineImpl.activeLimboTargetsByKey.remove(limboKey);
- syncEngineImpl.activeLimboResolutionsByTarget.delete(targetId);
- pumpEnqueuedLimboResolutions(syncEngineImpl);
- }
- else {
- await localStoreReleaseTarget(syncEngineImpl.localStore, targetId,
- /* keepPersistedTargetData */ false)
- .then(() => removeAndCleanupTarget(syncEngineImpl, targetId, err))
- .catch(ignoreIfPrimaryLeaseLoss);
- }
- }
- async function syncEngineApplySuccessfulWrite(syncEngine, mutationBatchResult) {
- const syncEngineImpl = debugCast(syncEngine);
- const batchId = mutationBatchResult.batch.batchId;
- try {
- const changes = await localStoreAcknowledgeBatch(syncEngineImpl.localStore, mutationBatchResult);
- // The local store may or may not be able to apply the write result and
- // raise events immediately (depending on whether the watcher is caught
- // up), so we raise user callbacks first so that they consistently happen
- // before listen events.
- processUserCallback(syncEngineImpl, batchId, /*error=*/ null);
- triggerPendingWritesCallbacks(syncEngineImpl, batchId);
- syncEngineImpl.sharedClientState.updateMutationState(batchId, 'acknowledged');
- await syncEngineEmitNewSnapsAndNotifyLocalStore(syncEngineImpl, changes);
- }
- catch (error) {
- await ignoreIfPrimaryLeaseLoss(error);
- }
- }
- async function syncEngineRejectFailedWrite(syncEngine, batchId, error) {
- const syncEngineImpl = debugCast(syncEngine);
- try {
- const changes = await localStoreRejectBatch(syncEngineImpl.localStore, batchId);
- // The local store may or may not be able to apply the write result and
- // raise events immediately (depending on whether the watcher is caught up),
- // so we raise user callbacks first so that they consistently happen before
- // listen events.
- processUserCallback(syncEngineImpl, batchId, error);
- triggerPendingWritesCallbacks(syncEngineImpl, batchId);
- syncEngineImpl.sharedClientState.updateMutationState(batchId, 'rejected', error);
- await syncEngineEmitNewSnapsAndNotifyLocalStore(syncEngineImpl, changes);
- }
- catch (error) {
- await ignoreIfPrimaryLeaseLoss(error);
- }
- }
- /**
- * Registers a user callback that resolves when all pending mutations at the moment of calling
- * are acknowledged .
- */
- async function syncEngineRegisterPendingWritesCallback(syncEngine, callback) {
- const syncEngineImpl = debugCast(syncEngine);
- if (!canUseNetwork(syncEngineImpl.remoteStore)) {
- logDebug(LOG_TAG$3, 'The network is disabled. The task returned by ' +
- "'awaitPendingWrites()' will not complete until the network is enabled.");
- }
- try {
- const highestBatchId = await localStoreGetHighestUnacknowledgedBatchId(syncEngineImpl.localStore);
- if (highestBatchId === BATCHID_UNKNOWN) {
- // Trigger the callback right away if there is no pending writes at the moment.
- callback.resolve();
- return;
- }
- const callbacks = syncEngineImpl.pendingWritesCallbacks.get(highestBatchId) || [];
- callbacks.push(callback);
- syncEngineImpl.pendingWritesCallbacks.set(highestBatchId, callbacks);
- }
- catch (e) {
- const firestoreError = wrapInUserErrorIfRecoverable(e, 'Initialization of waitForPendingWrites() operation failed');
- callback.reject(firestoreError);
- }
- }
- /**
- * Triggers the callbacks that are waiting for this batch id to get acknowledged by server,
- * if there are any.
- */
- function triggerPendingWritesCallbacks(syncEngineImpl, batchId) {
- (syncEngineImpl.pendingWritesCallbacks.get(batchId) || []).forEach(callback => {
- callback.resolve();
- });
- syncEngineImpl.pendingWritesCallbacks.delete(batchId);
- }
- /** Reject all outstanding callbacks waiting for pending writes to complete. */
- function rejectOutstandingPendingWritesCallbacks(syncEngineImpl, errorMessage) {
- syncEngineImpl.pendingWritesCallbacks.forEach(callbacks => {
- callbacks.forEach(callback => {
- callback.reject(new FirestoreError(Code.CANCELLED, errorMessage));
- });
- });
- syncEngineImpl.pendingWritesCallbacks.clear();
- }
- function addMutationCallback(syncEngineImpl, batchId, callback) {
- let newCallbacks = syncEngineImpl.mutationUserCallbacks[syncEngineImpl.currentUser.toKey()];
- if (!newCallbacks) {
- newCallbacks = new SortedMap(primitiveComparator);
- }
- newCallbacks = newCallbacks.insert(batchId, callback);
- syncEngineImpl.mutationUserCallbacks[syncEngineImpl.currentUser.toKey()] =
- newCallbacks;
- }
- /**
- * Resolves or rejects the user callback for the given batch and then discards
- * it.
- */
- function processUserCallback(syncEngine, batchId, error) {
- const syncEngineImpl = debugCast(syncEngine);
- let newCallbacks = syncEngineImpl.mutationUserCallbacks[syncEngineImpl.currentUser.toKey()];
- // NOTE: Mutations restored from persistence won't have callbacks, so it's
- // okay for there to be no callback for this ID.
- if (newCallbacks) {
- const callback = newCallbacks.get(batchId);
- if (callback) {
- if (error) {
- callback.reject(error);
- }
- else {
- callback.resolve();
- }
- newCallbacks = newCallbacks.remove(batchId);
- }
- syncEngineImpl.mutationUserCallbacks[syncEngineImpl.currentUser.toKey()] =
- newCallbacks;
- }
- }
- function removeAndCleanupTarget(syncEngineImpl, targetId, error = null) {
- syncEngineImpl.sharedClientState.removeLocalQueryTarget(targetId);
- for (const query of syncEngineImpl.queriesByTarget.get(targetId)) {
- syncEngineImpl.queryViewsByQuery.delete(query);
- if (error) {
- syncEngineImpl.syncEngineListener.onWatchError(query, error);
- }
- }
- syncEngineImpl.queriesByTarget.delete(targetId);
- if (syncEngineImpl.isPrimaryClient) {
- const limboKeys = syncEngineImpl.limboDocumentRefs.removeReferencesForId(targetId);
- limboKeys.forEach(limboKey => {
- const isReferenced = syncEngineImpl.limboDocumentRefs.containsKey(limboKey);
- if (!isReferenced) {
- // We removed the last reference for this key
- removeLimboTarget(syncEngineImpl, limboKey);
- }
- });
- }
- }
- function removeLimboTarget(syncEngineImpl, key) {
- syncEngineImpl.enqueuedLimboResolutions.delete(key.path.canonicalString());
- // It's possible that the target already got removed because the query failed. In that case,
- // the key won't exist in `limboTargetsByKey`. Only do the cleanup if we still have the target.
- const limboTargetId = syncEngineImpl.activeLimboTargetsByKey.get(key);
- if (limboTargetId === null) {
- // This target already got removed, because the query failed.
- return;
- }
- remoteStoreUnlisten(syncEngineImpl.remoteStore, limboTargetId);
- syncEngineImpl.activeLimboTargetsByKey =
- syncEngineImpl.activeLimboTargetsByKey.remove(key);
- syncEngineImpl.activeLimboResolutionsByTarget.delete(limboTargetId);
- pumpEnqueuedLimboResolutions(syncEngineImpl);
- }
- function updateTrackedLimbos(syncEngineImpl, targetId, limboChanges) {
- for (const limboChange of limboChanges) {
- if (limboChange instanceof AddedLimboDocument) {
- syncEngineImpl.limboDocumentRefs.addReference(limboChange.key, targetId);
- trackLimboChange(syncEngineImpl, limboChange);
- }
- else if (limboChange instanceof RemovedLimboDocument) {
- logDebug(LOG_TAG$3, 'Document no longer in limbo: ' + limboChange.key);
- syncEngineImpl.limboDocumentRefs.removeReference(limboChange.key, targetId);
- const isReferenced = syncEngineImpl.limboDocumentRefs.containsKey(limboChange.key);
- if (!isReferenced) {
- // We removed the last reference for this key
- removeLimboTarget(syncEngineImpl, limboChange.key);
- }
- }
- else {
- fail();
- }
- }
- }
- function trackLimboChange(syncEngineImpl, limboChange) {
- const key = limboChange.key;
- const keyString = key.path.canonicalString();
- if (!syncEngineImpl.activeLimboTargetsByKey.get(key) &&
- !syncEngineImpl.enqueuedLimboResolutions.has(keyString)) {
- logDebug(LOG_TAG$3, 'New document in limbo: ' + key);
- syncEngineImpl.enqueuedLimboResolutions.add(keyString);
- pumpEnqueuedLimboResolutions(syncEngineImpl);
- }
- }
- /**
- * Starts listens for documents in limbo that are enqueued for resolution,
- * subject to a maximum number of concurrent resolutions.
- *
- * Without bounding the number of concurrent resolutions, the server can fail
- * with "resource exhausted" errors which can lead to pathological client
- * behavior as seen in https://github.com/firebase/firebase-js-sdk/issues/2683.
- */
- function pumpEnqueuedLimboResolutions(syncEngineImpl) {
- while (syncEngineImpl.enqueuedLimboResolutions.size > 0 &&
- syncEngineImpl.activeLimboTargetsByKey.size <
- syncEngineImpl.maxConcurrentLimboResolutions) {
- const keyString = syncEngineImpl.enqueuedLimboResolutions
- .values()
- .next().value;
- syncEngineImpl.enqueuedLimboResolutions.delete(keyString);
- const key = new DocumentKey(ResourcePath.fromString(keyString));
- const limboTargetId = syncEngineImpl.limboTargetIdGenerator.next();
- syncEngineImpl.activeLimboResolutionsByTarget.set(limboTargetId, new LimboResolution(key));
- syncEngineImpl.activeLimboTargetsByKey =
- syncEngineImpl.activeLimboTargetsByKey.insert(key, limboTargetId);
- remoteStoreListen(syncEngineImpl.remoteStore, new TargetData(queryToTarget(newQueryForPath(key.path)), limboTargetId, "TargetPurposeLimboResolution" /* TargetPurpose.LimboResolution */, ListenSequence.INVALID));
- }
- }
- async function syncEngineEmitNewSnapsAndNotifyLocalStore(syncEngine, changes, remoteEvent) {
- const syncEngineImpl = debugCast(syncEngine);
- const newSnaps = [];
- const docChangesInAllViews = [];
- const queriesProcessed = [];
- if (syncEngineImpl.queryViewsByQuery.isEmpty()) {
- // Return early since `onWatchChange()` might not have been assigned yet.
- return;
- }
- syncEngineImpl.queryViewsByQuery.forEach((_, queryView) => {
- queriesProcessed.push(syncEngineImpl
- .applyDocChanges(queryView, changes, remoteEvent)
- .then(viewSnapshot => {
- // If there are changes, or we are handling a global snapshot, notify
- // secondary clients to update query state.
- if (viewSnapshot || remoteEvent) {
- if (syncEngineImpl.isPrimaryClient) {
- syncEngineImpl.sharedClientState.updateQueryState(queryView.targetId, (viewSnapshot === null || viewSnapshot === void 0 ? void 0 : viewSnapshot.fromCache) ? 'not-current' : 'current');
- }
- }
- // Update views if there are actual changes.
- if (!!viewSnapshot) {
- newSnaps.push(viewSnapshot);
- const docChanges = LocalViewChanges.fromSnapshot(queryView.targetId, viewSnapshot);
- docChangesInAllViews.push(docChanges);
- }
- }));
- });
- await Promise.all(queriesProcessed);
- syncEngineImpl.syncEngineListener.onWatchChange(newSnaps);
- await localStoreNotifyLocalViewChanges(syncEngineImpl.localStore, docChangesInAllViews);
- }
- async function applyDocChanges(syncEngineImpl, queryView, changes, remoteEvent) {
- let viewDocChanges = queryView.view.computeDocChanges(changes);
- if (viewDocChanges.needsRefill) {
- // The query has a limit and some docs were removed, so we need
- // to re-run the query against the local store to make sure we
- // didn't lose any good docs that had been past the limit.
- viewDocChanges = await localStoreExecuteQuery(syncEngineImpl.localStore, queryView.query,
- /* usePreviousResults= */ false).then(({ documents }) => {
- return queryView.view.computeDocChanges(documents, viewDocChanges);
- });
- }
- const targetChange = remoteEvent && remoteEvent.targetChanges.get(queryView.targetId);
- const viewChange = queryView.view.applyChanges(viewDocChanges,
- /* updateLimboDocuments= */ syncEngineImpl.isPrimaryClient, targetChange);
- updateTrackedLimbos(syncEngineImpl, queryView.targetId, viewChange.limboChanges);
- return viewChange.snapshot;
- }
- async function syncEngineHandleCredentialChange(syncEngine, user) {
- const syncEngineImpl = debugCast(syncEngine);
- const userChanged = !syncEngineImpl.currentUser.isEqual(user);
- if (userChanged) {
- logDebug(LOG_TAG$3, 'User change. New user:', user.toKey());
- const result = await localStoreHandleUserChange(syncEngineImpl.localStore, user);
- syncEngineImpl.currentUser = user;
- // Fails tasks waiting for pending writes requested by previous user.
- rejectOutstandingPendingWritesCallbacks(syncEngineImpl, "'waitForPendingWrites' promise is rejected due to a user change.");
- // TODO(b/114226417): Consider calling this only in the primary tab.
- syncEngineImpl.sharedClientState.handleUserChange(user, result.removedBatchIds, result.addedBatchIds);
- await syncEngineEmitNewSnapsAndNotifyLocalStore(syncEngineImpl, result.affectedDocuments);
- }
- }
- function syncEngineGetRemoteKeysForTarget(syncEngine, targetId) {
- const syncEngineImpl = debugCast(syncEngine);
- const limboResolution = syncEngineImpl.activeLimboResolutionsByTarget.get(targetId);
- if (limboResolution && limboResolution.receivedDocument) {
- return documentKeySet().add(limboResolution.key);
- }
- else {
- let keySet = documentKeySet();
- const queries = syncEngineImpl.queriesByTarget.get(targetId);
- if (!queries) {
- return keySet;
- }
- for (const query of queries) {
- const queryView = syncEngineImpl.queryViewsByQuery.get(query);
- keySet = keySet.unionWith(queryView.view.syncedDocuments);
- }
- return keySet;
- }
- }
- /**
- * Reconcile the list of synced documents in an existing view with those
- * from persistence.
- */
- async function synchronizeViewAndComputeSnapshot(syncEngine, queryView) {
- const syncEngineImpl = debugCast(syncEngine);
- const queryResult = await localStoreExecuteQuery(syncEngineImpl.localStore, queryView.query,
- /* usePreviousResults= */ true);
- const viewSnapshot = queryView.view.synchronizeWithPersistedState(queryResult);
- if (syncEngineImpl.isPrimaryClient) {
- updateTrackedLimbos(syncEngineImpl, queryView.targetId, viewSnapshot.limboChanges);
- }
- return viewSnapshot;
- }
- /**
- * Retrieves newly changed documents from remote document cache and raises
- * snapshots if needed.
- */
- // PORTING NOTE: Multi-Tab only.
- async function syncEngineSynchronizeWithChangedDocuments(syncEngine, collectionGroup) {
- const syncEngineImpl = debugCast(syncEngine);
- return localStoreGetNewDocumentChanges(syncEngineImpl.localStore, collectionGroup).then(changes => syncEngineEmitNewSnapsAndNotifyLocalStore(syncEngineImpl, changes));
- }
- /** Applies a mutation state to an existing batch. */
- // PORTING NOTE: Multi-Tab only.
- async function syncEngineApplyBatchState(syncEngine, batchId, batchState, error) {
- const syncEngineImpl = debugCast(syncEngine);
- const documents = await localStoreLookupMutationDocuments(syncEngineImpl.localStore, batchId);
- if (documents === null) {
- // A throttled tab may not have seen the mutation before it was completed
- // and removed from the mutation queue, in which case we won't have cached
- // the affected documents. In this case we can safely ignore the update
- // since that means we didn't apply the mutation locally at all (if we
- // had, we would have cached the affected documents), and so we will just
- // see any resulting document changes via normal remote document updates
- // as applicable.
- logDebug(LOG_TAG$3, 'Cannot apply mutation batch with id: ' + batchId);
- return;
- }
- if (batchState === 'pending') {
- // If we are the primary client, we need to send this write to the
- // backend. Secondary clients will ignore these writes since their remote
- // connection is disabled.
- await fillWritePipeline(syncEngineImpl.remoteStore);
- }
- else if (batchState === 'acknowledged' || batchState === 'rejected') {
- // NOTE: Both these methods are no-ops for batches that originated from
- // other clients.
- processUserCallback(syncEngineImpl, batchId, error ? error : null);
- triggerPendingWritesCallbacks(syncEngineImpl, batchId);
- localStoreRemoveCachedMutationBatchMetadata(syncEngineImpl.localStore, batchId);
- }
- else {
- fail();
- }
- await syncEngineEmitNewSnapsAndNotifyLocalStore(syncEngineImpl, documents);
- }
- /** Applies a query target change from a different tab. */
- // PORTING NOTE: Multi-Tab only.
- async function syncEngineApplyPrimaryState(syncEngine, isPrimary) {
- const syncEngineImpl = debugCast(syncEngine);
- ensureWatchCallbacks(syncEngineImpl);
- syncEngineEnsureWriteCallbacks(syncEngineImpl);
- if (isPrimary === true && syncEngineImpl._isPrimaryClient !== true) {
- // Secondary tabs only maintain Views for their local listeners and the
- // Views internal state may not be 100% populated (in particular
- // secondary tabs don't track syncedDocuments, the set of documents the
- // server considers to be in the target). So when a secondary becomes
- // primary, we need to need to make sure that all views for all targets
- // match the state on disk.
- const activeTargets = syncEngineImpl.sharedClientState.getAllActiveQueryTargets();
- const activeQueries = await synchronizeQueryViewsAndRaiseSnapshots(syncEngineImpl, activeTargets.toArray());
- syncEngineImpl._isPrimaryClient = true;
- await remoteStoreApplyPrimaryState(syncEngineImpl.remoteStore, true);
- for (const targetData of activeQueries) {
- remoteStoreListen(syncEngineImpl.remoteStore, targetData);
- }
- }
- else if (isPrimary === false && syncEngineImpl._isPrimaryClient !== false) {
- const activeTargets = [];
- let p = Promise.resolve();
- syncEngineImpl.queriesByTarget.forEach((_, targetId) => {
- if (syncEngineImpl.sharedClientState.isLocalQueryTarget(targetId)) {
- activeTargets.push(targetId);
- }
- else {
- p = p.then(() => {
- removeAndCleanupTarget(syncEngineImpl, targetId);
- return localStoreReleaseTarget(syncEngineImpl.localStore, targetId,
- /*keepPersistedTargetData=*/ true);
- });
- }
- remoteStoreUnlisten(syncEngineImpl.remoteStore, targetId);
- });
- await p;
- await synchronizeQueryViewsAndRaiseSnapshots(syncEngineImpl, activeTargets);
- resetLimboDocuments(syncEngineImpl);
- syncEngineImpl._isPrimaryClient = false;
- await remoteStoreApplyPrimaryState(syncEngineImpl.remoteStore, false);
- }
- }
- // PORTING NOTE: Multi-Tab only.
- function resetLimboDocuments(syncEngine) {
- const syncEngineImpl = debugCast(syncEngine);
- syncEngineImpl.activeLimboResolutionsByTarget.forEach((_, targetId) => {
- remoteStoreUnlisten(syncEngineImpl.remoteStore, targetId);
- });
- syncEngineImpl.limboDocumentRefs.removeAllReferences();
- syncEngineImpl.activeLimboResolutionsByTarget = new Map();
- syncEngineImpl.activeLimboTargetsByKey = new SortedMap(DocumentKey.comparator);
- }
- /**
- * Reconcile the query views of the provided query targets with the state from
- * persistence. Raises snapshots for any changes that affect the local
- * client and returns the updated state of all target's query data.
- *
- * @param syncEngine - The sync engine implementation
- * @param targets - the list of targets with views that need to be recomputed
- * @param transitionToPrimary - `true` iff the tab transitions from a secondary
- * tab to a primary tab
- */
- // PORTING NOTE: Multi-Tab only.
- async function synchronizeQueryViewsAndRaiseSnapshots(syncEngine, targets, transitionToPrimary) {
- const syncEngineImpl = debugCast(syncEngine);
- const activeQueries = [];
- const newViewSnapshots = [];
- for (const targetId of targets) {
- let targetData;
- const queries = syncEngineImpl.queriesByTarget.get(targetId);
- if (queries && queries.length !== 0) {
- // For queries that have a local View, we fetch their current state
- // from LocalStore (as the resume token and the snapshot version
- // might have changed) and reconcile their views with the persisted
- // state (the list of syncedDocuments may have gotten out of sync).
- targetData = await localStoreAllocateTarget(syncEngineImpl.localStore, queryToTarget(queries[0]));
- for (const query of queries) {
- const queryView = syncEngineImpl.queryViewsByQuery.get(query);
- const viewChange = await synchronizeViewAndComputeSnapshot(syncEngineImpl, queryView);
- if (viewChange.snapshot) {
- newViewSnapshots.push(viewChange.snapshot);
- }
- }
- }
- else {
- // For queries that never executed on this client, we need to
- // allocate the target in LocalStore and initialize a new View.
- const target = await localStoreGetCachedTarget(syncEngineImpl.localStore, targetId);
- targetData = await localStoreAllocateTarget(syncEngineImpl.localStore, target);
- await initializeViewAndComputeSnapshot(syncEngineImpl, synthesizeTargetToQuery(target), targetId,
- /*current=*/ false, targetData.resumeToken);
- }
- activeQueries.push(targetData);
- }
- syncEngineImpl.syncEngineListener.onWatchChange(newViewSnapshots);
- return activeQueries;
- }
- /**
- * Creates a `Query` object from the specified `Target`. There is no way to
- * obtain the original `Query`, so we synthesize a `Query` from the `Target`
- * object.
- *
- * The synthesized result might be different from the original `Query`, but
- * since the synthesized `Query` should return the same results as the
- * original one (only the presentation of results might differ), the potential
- * difference will not cause issues.
- */
- // PORTING NOTE: Multi-Tab only.
- function synthesizeTargetToQuery(target) {
- return newQuery(target.path, target.collectionGroup, target.orderBy, target.filters, target.limit, "F" /* LimitType.First */, target.startAt, target.endAt);
- }
- /** Returns the IDs of the clients that are currently active. */
- // PORTING NOTE: Multi-Tab only.
- function syncEngineGetActiveClients(syncEngine) {
- const syncEngineImpl = debugCast(syncEngine);
- return localStoreGetActiveClients(syncEngineImpl.localStore);
- }
- /** Applies a query target change from a different tab. */
- // PORTING NOTE: Multi-Tab only.
- async function syncEngineApplyTargetState(syncEngine, targetId, state, error) {
- const syncEngineImpl = debugCast(syncEngine);
- if (syncEngineImpl._isPrimaryClient) {
- // If we receive a target state notification via WebStorage, we are
- // either already secondary or another tab has taken the primary lease.
- logDebug(LOG_TAG$3, 'Ignoring unexpected query state notification.');
- return;
- }
- const query = syncEngineImpl.queriesByTarget.get(targetId);
- if (query && query.length > 0) {
- switch (state) {
- case 'current':
- case 'not-current': {
- const changes = await localStoreGetNewDocumentChanges(syncEngineImpl.localStore, queryCollectionGroup(query[0]));
- const synthesizedRemoteEvent = RemoteEvent.createSynthesizedRemoteEventForCurrentChange(targetId, state === 'current', ByteString.EMPTY_BYTE_STRING);
- await syncEngineEmitNewSnapsAndNotifyLocalStore(syncEngineImpl, changes, synthesizedRemoteEvent);
- break;
- }
- case 'rejected': {
- await localStoreReleaseTarget(syncEngineImpl.localStore, targetId,
- /* keepPersistedTargetData */ true);
- removeAndCleanupTarget(syncEngineImpl, targetId, error);
- break;
- }
- default:
- fail();
- }
- }
- }
- /** Adds or removes Watch targets for queries from different tabs. */
- async function syncEngineApplyActiveTargetsChange(syncEngine, added, removed) {
- const syncEngineImpl = ensureWatchCallbacks(syncEngine);
- if (!syncEngineImpl._isPrimaryClient) {
- return;
- }
- for (const targetId of added) {
- if (syncEngineImpl.queriesByTarget.has(targetId)) {
- // A target might have been added in a previous attempt
- logDebug(LOG_TAG$3, 'Adding an already active target ' + targetId);
- continue;
- }
- const target = await localStoreGetCachedTarget(syncEngineImpl.localStore, targetId);
- const targetData = await localStoreAllocateTarget(syncEngineImpl.localStore, target);
- await initializeViewAndComputeSnapshot(syncEngineImpl, synthesizeTargetToQuery(target), targetData.targetId,
- /*current=*/ false, targetData.resumeToken);
- remoteStoreListen(syncEngineImpl.remoteStore, targetData);
- }
- for (const targetId of removed) {
- // Check that the target is still active since the target might have been
- // removed if it has been rejected by the backend.
- if (!syncEngineImpl.queriesByTarget.has(targetId)) {
- continue;
- }
- // Release queries that are still active.
- await localStoreReleaseTarget(syncEngineImpl.localStore, targetId,
- /* keepPersistedTargetData */ false)
- .then(() => {
- remoteStoreUnlisten(syncEngineImpl.remoteStore, targetId);
- removeAndCleanupTarget(syncEngineImpl, targetId);
- })
- .catch(ignoreIfPrimaryLeaseLoss);
- }
- }
- function ensureWatchCallbacks(syncEngine) {
- const syncEngineImpl = debugCast(syncEngine);
- syncEngineImpl.remoteStore.remoteSyncer.applyRemoteEvent =
- syncEngineApplyRemoteEvent.bind(null, syncEngineImpl);
- syncEngineImpl.remoteStore.remoteSyncer.getRemoteKeysForTarget =
- syncEngineGetRemoteKeysForTarget.bind(null, syncEngineImpl);
- syncEngineImpl.remoteStore.remoteSyncer.rejectListen =
- syncEngineRejectListen.bind(null, syncEngineImpl);
- syncEngineImpl.syncEngineListener.onWatchChange =
- eventManagerOnWatchChange.bind(null, syncEngineImpl.eventManager);
- syncEngineImpl.syncEngineListener.onWatchError =
- eventManagerOnWatchError.bind(null, syncEngineImpl.eventManager);
- return syncEngineImpl;
- }
- function syncEngineEnsureWriteCallbacks(syncEngine) {
- const syncEngineImpl = debugCast(syncEngine);
- syncEngineImpl.remoteStore.remoteSyncer.applySuccessfulWrite =
- syncEngineApplySuccessfulWrite.bind(null, syncEngineImpl);
- syncEngineImpl.remoteStore.remoteSyncer.rejectFailedWrite =
- syncEngineRejectFailedWrite.bind(null, syncEngineImpl);
- return syncEngineImpl;
- }
- /**
- * Loads a Firestore bundle into the SDK. The returned promise resolves when
- * the bundle finished loading.
- *
- * @param syncEngine - SyncEngine to use.
- * @param bundleReader - Bundle to load into the SDK.
- * @param task - LoadBundleTask used to update the loading progress to public API.
- */
- function syncEngineLoadBundle(syncEngine, bundleReader, task) {
- const syncEngineImpl = debugCast(syncEngine);
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- loadBundleImpl(syncEngineImpl, bundleReader, task).then(collectionGroups => {
- syncEngineImpl.sharedClientState.notifyBundleLoaded(collectionGroups);
- });
- }
- /** Loads a bundle and returns the list of affected collection groups. */
- async function loadBundleImpl(syncEngine, reader, task) {
- try {
- const metadata = await reader.getMetadata();
- const skip = await localStoreHasNewerBundle(syncEngine.localStore, metadata);
- if (skip) {
- await reader.close();
- task._completeWith(bundleSuccessProgress(metadata));
- return Promise.resolve(new Set());
- }
- task._updateProgress(bundleInitialProgress(metadata));
- const loader = new BundleLoader(metadata, syncEngine.localStore, reader.serializer);
- let element = await reader.nextElement();
- while (element) {
- ;
- const progress = await loader.addSizedElement(element);
- if (progress) {
- task._updateProgress(progress);
- }
- element = await reader.nextElement();
- }
- const result = await loader.complete();
- await syncEngineEmitNewSnapsAndNotifyLocalStore(syncEngine, result.changedDocs,
- /* remoteEvent */ undefined);
- // Save metadata, so loading the same bundle will skip.
- await localStoreSaveBundle(syncEngine.localStore, metadata);
- task._completeWith(result.progress);
- return Promise.resolve(result.changedCollectionGroups);
- }
- catch (e) {
- logWarn(LOG_TAG$3, `Loading bundle failed with ${e}`);
- task._failWith(e);
- return Promise.resolve(new Set());
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Provides all components needed for Firestore with in-memory persistence.
- * Uses EagerGC garbage collection.
- */
- class MemoryOfflineComponentProvider {
- constructor() {
- this.synchronizeTabs = false;
- }
- async initialize(cfg) {
- this.serializer = newSerializer(cfg.databaseInfo.databaseId);
- this.sharedClientState = this.createSharedClientState(cfg);
- this.persistence = this.createPersistence(cfg);
- await this.persistence.start();
- this.localStore = this.createLocalStore(cfg);
- this.gcScheduler = this.createGarbageCollectionScheduler(cfg, this.localStore);
- this.indexBackfillerScheduler = this.createIndexBackfillerScheduler(cfg, this.localStore);
- }
- createGarbageCollectionScheduler(cfg, localStore) {
- return null;
- }
- createIndexBackfillerScheduler(cfg, localStore) {
- return null;
- }
- createLocalStore(cfg) {
- return newLocalStore(this.persistence, new QueryEngine(), cfg.initialUser, this.serializer);
- }
- createPersistence(cfg) {
- return new MemoryPersistence(MemoryEagerDelegate.factory, this.serializer);
- }
- createSharedClientState(cfg) {
- return new MemorySharedClientState();
- }
- async terminate() {
- if (this.gcScheduler) {
- this.gcScheduler.stop();
- }
- await this.sharedClientState.shutdown();
- await this.persistence.shutdown();
- }
- }
- class LruGcMemoryOfflineComponentProvider extends MemoryOfflineComponentProvider {
- constructor(cacheSizeBytes) {
- super();
- this.cacheSizeBytes = cacheSizeBytes;
- }
- createGarbageCollectionScheduler(cfg, localStore) {
- hardAssert(this.persistence.referenceDelegate instanceof MemoryLruDelegate);
- const garbageCollector = this.persistence.referenceDelegate.garbageCollector;
- return new LruScheduler(garbageCollector, cfg.asyncQueue, localStore);
- }
- createPersistence(cfg) {
- const lruParams = this.cacheSizeBytes !== undefined
- ? LruParams.withCacheSize(this.cacheSizeBytes)
- : LruParams.DEFAULT;
- return new MemoryPersistence(p => MemoryLruDelegate.factory(p, lruParams), this.serializer);
- }
- }
- /**
- * Provides all components needed for Firestore with IndexedDB persistence.
- */
- class IndexedDbOfflineComponentProvider extends MemoryOfflineComponentProvider {
- constructor(onlineComponentProvider, cacheSizeBytes, forceOwnership) {
- super();
- this.onlineComponentProvider = onlineComponentProvider;
- this.cacheSizeBytes = cacheSizeBytes;
- this.forceOwnership = forceOwnership;
- this.synchronizeTabs = false;
- }
- async initialize(cfg) {
- await super.initialize(cfg);
- await this.onlineComponentProvider.initialize(this, cfg);
- // Enqueue writes from a previous session
- await syncEngineEnsureWriteCallbacks(this.onlineComponentProvider.syncEngine);
- await fillWritePipeline(this.onlineComponentProvider.remoteStore);
- // NOTE: This will immediately call the listener, so we make sure to
- // set it after localStore / remoteStore are started.
- await this.persistence.setPrimaryStateListener(() => {
- if (this.gcScheduler && !this.gcScheduler.started) {
- this.gcScheduler.start();
- }
- if (this.indexBackfillerScheduler &&
- !this.indexBackfillerScheduler.started) {
- this.indexBackfillerScheduler.start();
- }
- return Promise.resolve();
- });
- }
- createLocalStore(cfg) {
- return newLocalStore(this.persistence, new QueryEngine(), cfg.initialUser, this.serializer);
- }
- createGarbageCollectionScheduler(cfg, localStore) {
- const garbageCollector = this.persistence.referenceDelegate.garbageCollector;
- return new LruScheduler(garbageCollector, cfg.asyncQueue, localStore);
- }
- createIndexBackfillerScheduler(cfg, localStore) {
- const indexBackfiller = new IndexBackfiller(localStore, this.persistence);
- return new IndexBackfillerScheduler(cfg.asyncQueue, indexBackfiller);
- }
- createPersistence(cfg) {
- const persistenceKey = indexedDbStoragePrefix(cfg.databaseInfo.databaseId, cfg.databaseInfo.persistenceKey);
- const lruParams = this.cacheSizeBytes !== undefined
- ? LruParams.withCacheSize(this.cacheSizeBytes)
- : LruParams.DEFAULT;
- return new IndexedDbPersistence(this.synchronizeTabs, persistenceKey, cfg.clientId, lruParams, cfg.asyncQueue, getWindow(), getDocument(), this.serializer, this.sharedClientState, !!this.forceOwnership);
- }
- createSharedClientState(cfg) {
- return new MemorySharedClientState();
- }
- }
- /**
- * Provides all components needed for Firestore with multi-tab IndexedDB
- * persistence.
- *
- * In the legacy client, this provider is used to provide both multi-tab and
- * non-multi-tab persistence since we cannot tell at build time whether
- * `synchronizeTabs` will be enabled.
- */
- class MultiTabOfflineComponentProvider extends IndexedDbOfflineComponentProvider {
- constructor(onlineComponentProvider, cacheSizeBytes) {
- super(onlineComponentProvider, cacheSizeBytes, /* forceOwnership= */ false);
- this.onlineComponentProvider = onlineComponentProvider;
- this.cacheSizeBytes = cacheSizeBytes;
- this.synchronizeTabs = true;
- }
- async initialize(cfg) {
- await super.initialize(cfg);
- const syncEngine = this.onlineComponentProvider.syncEngine;
- if (this.sharedClientState instanceof WebStorageSharedClientState) {
- this.sharedClientState.syncEngine = {
- applyBatchState: syncEngineApplyBatchState.bind(null, syncEngine),
- applyTargetState: syncEngineApplyTargetState.bind(null, syncEngine),
- applyActiveTargetsChange: syncEngineApplyActiveTargetsChange.bind(null, syncEngine),
- getActiveClients: syncEngineGetActiveClients.bind(null, syncEngine),
- synchronizeWithChangedDocuments: syncEngineSynchronizeWithChangedDocuments.bind(null, syncEngine)
- };
- await this.sharedClientState.start();
- }
- // NOTE: This will immediately call the listener, so we make sure to
- // set it after localStore / remoteStore are started.
- await this.persistence.setPrimaryStateListener(async (isPrimary) => {
- await syncEngineApplyPrimaryState(this.onlineComponentProvider.syncEngine, isPrimary);
- if (this.gcScheduler) {
- if (isPrimary && !this.gcScheduler.started) {
- this.gcScheduler.start();
- }
- else if (!isPrimary) {
- this.gcScheduler.stop();
- }
- }
- if (this.indexBackfillerScheduler) {
- if (isPrimary && !this.indexBackfillerScheduler.started) {
- this.indexBackfillerScheduler.start();
- }
- else if (!isPrimary) {
- this.indexBackfillerScheduler.stop();
- }
- }
- });
- }
- createSharedClientState(cfg) {
- const window = getWindow();
- if (!WebStorageSharedClientState.isAvailable(window)) {
- throw new FirestoreError(Code.UNIMPLEMENTED, 'IndexedDB persistence is only available on platforms that support LocalStorage.');
- }
- const persistenceKey = indexedDbStoragePrefix(cfg.databaseInfo.databaseId, cfg.databaseInfo.persistenceKey);
- return new WebStorageSharedClientState(window, cfg.asyncQueue, persistenceKey, cfg.clientId, cfg.initialUser);
- }
- }
- /**
- * Initializes and wires the components that are needed to interface with the
- * network.
- */
- class OnlineComponentProvider {
- async initialize(offlineComponentProvider, cfg) {
- if (this.localStore) {
- // OnlineComponentProvider may get initialized multiple times if
- // multi-tab persistence is used.
- return;
- }
- this.localStore = offlineComponentProvider.localStore;
- this.sharedClientState = offlineComponentProvider.sharedClientState;
- this.datastore = this.createDatastore(cfg);
- this.remoteStore = this.createRemoteStore(cfg);
- this.eventManager = this.createEventManager(cfg);
- this.syncEngine = this.createSyncEngine(cfg,
- /* startAsPrimary=*/ !offlineComponentProvider.synchronizeTabs);
- this.sharedClientState.onlineStateHandler = onlineState => syncEngineApplyOnlineStateChange(this.syncEngine, onlineState, 1 /* OnlineStateSource.SharedClientState */);
- this.remoteStore.remoteSyncer.handleCredentialChange =
- syncEngineHandleCredentialChange.bind(null, this.syncEngine);
- await remoteStoreApplyPrimaryState(this.remoteStore, this.syncEngine.isPrimaryClient);
- }
- createEventManager(cfg) {
- return newEventManager();
- }
- createDatastore(cfg) {
- const serializer = newSerializer(cfg.databaseInfo.databaseId);
- const connection = newConnection(cfg.databaseInfo);
- return newDatastore(cfg.authCredentials, cfg.appCheckCredentials, connection, serializer);
- }
- createRemoteStore(cfg) {
- return newRemoteStore(this.localStore, this.datastore, cfg.asyncQueue, onlineState => syncEngineApplyOnlineStateChange(this.syncEngine, onlineState, 0 /* OnlineStateSource.RemoteStore */), newConnectivityMonitor());
- }
- createSyncEngine(cfg, startAsPrimary) {
- return newSyncEngine(this.localStore, this.remoteStore, this.eventManager, this.sharedClientState, cfg.initialUser, cfg.maxConcurrentLimboResolutions, startAsPrimary);
- }
- terminate() {
- return remoteStoreShutdown(this.remoteStore);
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * How many bytes to read each time when `ReadableStreamReader.read()` is
- * called. Only applicable for byte streams that we control (e.g. those backed
- * by an UInt8Array).
- */
- const DEFAULT_BYTES_PER_READ = 10240;
- /**
- * Builds a `ByteStreamReader` from a UInt8Array.
- * @param source - The data source to use.
- * @param bytesPerRead - How many bytes each `read()` from the returned reader
- * will read.
- */
- function toByteStreamReaderHelper(source, bytesPerRead = DEFAULT_BYTES_PER_READ) {
- let readFrom = 0;
- // The TypeScript definition for ReadableStreamReader changed. We use
- // `any` here to allow this code to compile with different versions.
- // See https://github.com/microsoft/TypeScript/issues/42970
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const reader = {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- async read() {
- if (readFrom < source.byteLength) {
- const result = {
- value: source.slice(readFrom, readFrom + bytesPerRead),
- done: false
- };
- readFrom += bytesPerRead;
- return result;
- }
- return { done: true };
- },
- async cancel() { },
- releaseLock() { },
- closed: Promise.resolve()
- };
- return reader;
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- function validateNonEmptyArgument(functionName, argumentName, argument) {
- if (!argument) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Function ${functionName}() cannot be called with an empty ${argumentName}.`);
- }
- }
- /**
- * Validates that two boolean options are not set at the same time.
- * @internal
- */
- function validateIsNotUsedTogether(optionName1, argument1, optionName2, argument2) {
- if (argument1 === true && argument2 === true) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `${optionName1} and ${optionName2} cannot be used together.`);
- }
- }
- /**
- * Validates that `path` refers to a document (indicated by the fact it contains
- * an even numbers of segments).
- */
- function validateDocumentPath(path) {
- if (!DocumentKey.isDocumentKey(path)) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid document reference. Document references must have an even number of segments, but ${path} has ${path.length}.`);
- }
- }
- /**
- * Validates that `path` refers to a collection (indicated by the fact it
- * contains an odd numbers of segments).
- */
- function validateCollectionPath(path) {
- if (DocumentKey.isDocumentKey(path)) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid collection reference. Collection references must have an odd number of segments, but ${path} has ${path.length}.`);
- }
- }
- /**
- * Returns true if it's a non-null object without a custom prototype
- * (i.e. excludes Array, Date, etc.).
- */
- function isPlainObject(input) {
- return (typeof input === 'object' &&
- input !== null &&
- (Object.getPrototypeOf(input) === Object.prototype ||
- Object.getPrototypeOf(input) === null));
- }
- /** Returns a string describing the type / value of the provided input. */
- function valueDescription(input) {
- if (input === undefined) {
- return 'undefined';
- }
- else if (input === null) {
- return 'null';
- }
- else if (typeof input === 'string') {
- if (input.length > 20) {
- input = `${input.substring(0, 20)}...`;
- }
- return JSON.stringify(input);
- }
- else if (typeof input === 'number' || typeof input === 'boolean') {
- return '' + input;
- }
- else if (typeof input === 'object') {
- if (input instanceof Array) {
- return 'an array';
- }
- else {
- const customObjectName = tryGetCustomObjectType(input);
- if (customObjectName) {
- return `a custom ${customObjectName} object`;
- }
- else {
- return 'an object';
- }
- }
- }
- else if (typeof input === 'function') {
- return 'a function';
- }
- else {
- return fail();
- }
- }
- /** try to get the constructor name for an object. */
- function tryGetCustomObjectType(input) {
- if (input.constructor) {
- return input.constructor.name;
- }
- return null;
- }
- /**
- * Casts `obj` to `T`, optionally unwrapping Compat types to expose the
- * underlying instance. Throws if `obj` is not an instance of `T`.
- *
- * This cast is used in the Lite and Full SDK to verify instance types for
- * arguments passed to the public API.
- * @internal
- */
- function cast(obj,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- constructor) {
- if ('_delegate' in obj) {
- // Unwrap Compat types
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- obj = obj._delegate;
- }
- if (!(obj instanceof constructor)) {
- if (constructor.name === obj.constructor.name) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Type does not match the expected instance. Did you pass a ' +
- `reference from a different Firestore SDK?`);
- }
- else {
- const description = valueDescription(obj);
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Expected type '${constructor.name}', but it was: ${description}`);
- }
- }
- return obj;
- }
- function validatePositiveNumber(functionName, n) {
- if (n <= 0) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Function ${functionName}() requires a positive number, but it was: ${n}.`);
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * On Node, only supported data source is a `Uint8Array` for now.
- */
- function toByteStreamReader(source, bytesPerRead) {
- if (!(source instanceof Uint8Array)) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `NodePlatform.toByteStreamReader expects source to be Uint8Array, got ${valueDescription(source)}`);
- }
- return toByteStreamReaderHelper(source, bytesPerRead);
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /*
- * A wrapper implementation of Observer<T> that will dispatch events
- * asynchronously. To allow immediate silencing, a mute call is added which
- * causes events scheduled to no longer be raised.
- */
- class AsyncObserver {
- constructor(observer) {
- this.observer = observer;
- /**
- * When set to true, will not raise future events. Necessary to deal with
- * async detachment of listener.
- */
- this.muted = false;
- }
- next(value) {
- if (this.observer.next) {
- this.scheduleEvent(this.observer.next, value);
- }
- }
- error(error) {
- if (this.observer.error) {
- this.scheduleEvent(this.observer.error, error);
- }
- else {
- logError('Uncaught Error in snapshot listener:', error.toString());
- }
- }
- mute() {
- this.muted = true;
- }
- scheduleEvent(eventHandler, event) {
- if (!this.muted) {
- setTimeout(() => {
- if (!this.muted) {
- eventHandler(event);
- }
- }, 0);
- }
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * A complete element in the bundle stream, together with the byte length it
- * occupies in the stream.
- */
- class SizedBundleElement {
- constructor(payload,
- // How many bytes this element takes to store in the bundle.
- byteLength) {
- this.payload = payload;
- this.byteLength = byteLength;
- }
- isBundleMetadata() {
- return 'metadata' in this.payload;
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * A class representing a bundle.
- *
- * Takes a bundle stream or buffer, and presents abstractions to read bundled
- * elements out of the underlying content.
- */
- class BundleReaderImpl {
- constructor(
- /** The reader to read from underlying binary bundle data source. */
- reader, serializer) {
- this.reader = reader;
- this.serializer = serializer;
- /** Cached bundle metadata. */
- this.metadata = new Deferred();
- /**
- * Internal buffer to hold bundle content, accumulating incomplete element
- * content.
- */
- this.buffer = new Uint8Array();
- this.textDecoder = newTextDecoder();
- // Read the metadata (which is the first element).
- this.nextElementImpl().then(element => {
- if (element && element.isBundleMetadata()) {
- this.metadata.resolve(element.payload.metadata);
- }
- else {
- this.metadata.reject(new Error(`The first element of the bundle is not a metadata, it is
- ${JSON.stringify(element === null || element === void 0 ? void 0 : element.payload)}`));
- }
- }, error => this.metadata.reject(error));
- }
- close() {
- return this.reader.cancel();
- }
- async getMetadata() {
- return this.metadata.promise;
- }
- async nextElement() {
- // Makes sure metadata is read before proceeding.
- await this.getMetadata();
- return this.nextElementImpl();
- }
- /**
- * Reads from the head of internal buffer, and pulling more data from
- * underlying stream if a complete element cannot be found, until an
- * element(including the prefixed length and the JSON string) is found.
- *
- * Once a complete element is read, it is dropped from internal buffer.
- *
- * Returns either the bundled element, or null if we have reached the end of
- * the stream.
- */
- async nextElementImpl() {
- const lengthBuffer = await this.readLength();
- if (lengthBuffer === null) {
- return null;
- }
- const lengthString = this.textDecoder.decode(lengthBuffer);
- const length = Number(lengthString);
- if (isNaN(length)) {
- this.raiseError(`length string (${lengthString}) is not valid number`);
- }
- const jsonString = await this.readJsonString(length);
- return new SizedBundleElement(JSON.parse(jsonString), lengthBuffer.length + length);
- }
- /** First index of '{' from the underlying buffer. */
- indexOfOpenBracket() {
- return this.buffer.findIndex(v => v === '{'.charCodeAt(0));
- }
- /**
- * Reads from the beginning of the internal buffer, until the first '{', and
- * return the content.
- *
- * If reached end of the stream, returns a null.
- */
- async readLength() {
- while (this.indexOfOpenBracket() < 0) {
- const done = await this.pullMoreDataToBuffer();
- if (done) {
- break;
- }
- }
- // Broke out of the loop because underlying stream is closed, and there
- // happens to be no more data to process.
- if (this.buffer.length === 0) {
- return null;
- }
- const position = this.indexOfOpenBracket();
- // Broke out of the loop because underlying stream is closed, but still
- // cannot find an open bracket.
- if (position < 0) {
- this.raiseError('Reached the end of bundle when a length string is expected.');
- }
- const result = this.buffer.slice(0, position);
- // Update the internal buffer to drop the read length.
- this.buffer = this.buffer.slice(position);
- return result;
- }
- /**
- * Reads from a specified position from the internal buffer, for a specified
- * number of bytes, pulling more data from the underlying stream if needed.
- *
- * Returns a string decoded from the read bytes.
- */
- async readJsonString(length) {
- while (this.buffer.length < length) {
- const done = await this.pullMoreDataToBuffer();
- if (done) {
- this.raiseError('Reached the end of bundle when more is expected.');
- }
- }
- const result = this.textDecoder.decode(this.buffer.slice(0, length));
- // Update the internal buffer to drop the read json string.
- this.buffer = this.buffer.slice(length);
- return result;
- }
- raiseError(message) {
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.reader.cancel();
- throw new Error(`Invalid bundle format: ${message}`);
- }
- /**
- * Pulls more data from underlying stream to internal buffer.
- * Returns a boolean indicating whether the stream is finished.
- */
- async pullMoreDataToBuffer() {
- const result = await this.reader.read();
- if (!result.done) {
- const newBuffer = new Uint8Array(this.buffer.length + result.value.length);
- newBuffer.set(this.buffer);
- newBuffer.set(result.value, this.buffer.length);
- this.buffer = newBuffer;
- }
- return result.done;
- }
- }
- function newBundleReader(reader, serializer) {
- return new BundleReaderImpl(reader, serializer);
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Internal transaction object responsible for accumulating the mutations to
- * perform and the base versions for any documents read.
- */
- class Transaction$2 {
- constructor(datastore) {
- this.datastore = datastore;
- // The version of each document that was read during this transaction.
- this.readVersions = new Map();
- this.mutations = [];
- this.committed = false;
- /**
- * A deferred usage error that occurred previously in this transaction that
- * will cause the transaction to fail once it actually commits.
- */
- this.lastWriteError = null;
- /**
- * Set of documents that have been written in the transaction.
- *
- * When there's more than one write to the same key in a transaction, any
- * writes after the first are handled differently.
- */
- this.writtenDocs = new Set();
- }
- async lookup(keys) {
- this.ensureCommitNotCalled();
- if (this.mutations.length > 0) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Firestore transactions require all reads to be executed before all writes.');
- }
- const docs = await invokeBatchGetDocumentsRpc(this.datastore, keys);
- docs.forEach(doc => this.recordVersion(doc));
- return docs;
- }
- set(key, data) {
- this.write(data.toMutation(key, this.precondition(key)));
- this.writtenDocs.add(key.toString());
- }
- update(key, data) {
- try {
- this.write(data.toMutation(key, this.preconditionForUpdate(key)));
- }
- catch (e) {
- this.lastWriteError = e;
- }
- this.writtenDocs.add(key.toString());
- }
- delete(key) {
- this.write(new DeleteMutation(key, this.precondition(key)));
- this.writtenDocs.add(key.toString());
- }
- async commit() {
- this.ensureCommitNotCalled();
- if (this.lastWriteError) {
- throw this.lastWriteError;
- }
- const unwritten = this.readVersions;
- // For each mutation, note that the doc was written.
- this.mutations.forEach(mutation => {
- unwritten.delete(mutation.key.toString());
- });
- // For each document that was read but not written to, we want to perform
- // a `verify` operation.
- unwritten.forEach((_, path) => {
- const key = DocumentKey.fromPath(path);
- this.mutations.push(new VerifyMutation(key, this.precondition(key)));
- });
- await invokeCommitRpc(this.datastore, this.mutations);
- this.committed = true;
- }
- recordVersion(doc) {
- let docVersion;
- if (doc.isFoundDocument()) {
- docVersion = doc.version;
- }
- else if (doc.isNoDocument()) {
- // Represent a deleted doc using SnapshotVersion.min().
- docVersion = SnapshotVersion.min();
- }
- else {
- throw fail();
- }
- const existingVersion = this.readVersions.get(doc.key.toString());
- if (existingVersion) {
- if (!docVersion.isEqual(existingVersion)) {
- // This transaction will fail no matter what.
- throw new FirestoreError(Code.ABORTED, 'Document version changed between two reads.');
- }
- }
- else {
- this.readVersions.set(doc.key.toString(), docVersion);
- }
- }
- /**
- * Returns the version of this document when it was read in this transaction,
- * as a precondition, or no precondition if it was not read.
- */
- precondition(key) {
- const version = this.readVersions.get(key.toString());
- if (!this.writtenDocs.has(key.toString()) && version) {
- if (version.isEqual(SnapshotVersion.min())) {
- return Precondition.exists(false);
- }
- else {
- return Precondition.updateTime(version);
- }
- }
- else {
- return Precondition.none();
- }
- }
- /**
- * Returns the precondition for a document if the operation is an update.
- */
- preconditionForUpdate(key) {
- const version = this.readVersions.get(key.toString());
- // The first time a document is written, we want to take into account the
- // read time and existence
- if (!this.writtenDocs.has(key.toString()) && version) {
- if (version.isEqual(SnapshotVersion.min())) {
- // The document doesn't exist, so fail the transaction.
- // This has to be validated locally because you can't send a
- // precondition that a document does not exist without changing the
- // semantics of the backend write to be an insert. This is the reverse
- // of what we want, since we want to assert that the document doesn't
- // exist but then send the update and have it fail. Since we can't
- // express that to the backend, we have to validate locally.
- // Note: this can change once we can send separate verify writes in the
- // transaction.
- throw new FirestoreError(Code.INVALID_ARGUMENT, "Can't update a document that doesn't exist.");
- }
- // Document exists, base precondition on document update time.
- return Precondition.updateTime(version);
- }
- else {
- // Document was not read, so we just use the preconditions for a blind
- // update.
- return Precondition.exists(true);
- }
- }
- write(mutation) {
- this.ensureCommitNotCalled();
- this.mutations.push(mutation);
- }
- ensureCommitNotCalled() {
- }
- }
- /**
- * @license
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * TransactionRunner encapsulates the logic needed to run and retry transactions
- * with backoff.
- */
- class TransactionRunner {
- constructor(asyncQueue, datastore, options, updateFunction, deferred) {
- this.asyncQueue = asyncQueue;
- this.datastore = datastore;
- this.options = options;
- this.updateFunction = updateFunction;
- this.deferred = deferred;
- this.attemptsRemaining = options.maxAttempts;
- this.backoff = new ExponentialBackoff(this.asyncQueue, "transaction_retry" /* TimerId.TransactionRetry */);
- }
- /** Runs the transaction and sets the result on deferred. */
- run() {
- this.attemptsRemaining -= 1;
- this.runWithBackOff();
- }
- runWithBackOff() {
- this.backoff.backoffAndRun(async () => {
- const transaction = new Transaction$2(this.datastore);
- const userPromise = this.tryRunUpdateFunction(transaction);
- if (userPromise) {
- userPromise
- .then(result => {
- this.asyncQueue.enqueueAndForget(() => {
- return transaction
- .commit()
- .then(() => {
- this.deferred.resolve(result);
- })
- .catch(commitError => {
- this.handleTransactionError(commitError);
- });
- });
- })
- .catch(userPromiseError => {
- this.handleTransactionError(userPromiseError);
- });
- }
- });
- }
- tryRunUpdateFunction(transaction) {
- try {
- const userPromise = this.updateFunction(transaction);
- if (isNullOrUndefined(userPromise) ||
- !userPromise.catch ||
- !userPromise.then) {
- this.deferred.reject(Error('Transaction callback must return a Promise'));
- return null;
- }
- return userPromise;
- }
- catch (error) {
- // Do not retry errors thrown by user provided updateFunction.
- this.deferred.reject(error);
- return null;
- }
- }
- handleTransactionError(error) {
- if (this.attemptsRemaining > 0 && this.isRetryableTransactionError(error)) {
- this.attemptsRemaining -= 1;
- this.asyncQueue.enqueueAndForget(() => {
- this.runWithBackOff();
- return Promise.resolve();
- });
- }
- else {
- this.deferred.reject(error);
- }
- }
- isRetryableTransactionError(error) {
- if (error.name === 'FirebaseError') {
- // In transactions, the backend will fail outdated reads with FAILED_PRECONDITION and
- // non-matching document versions with ABORTED. These errors should be retried.
- const code = error.code;
- return (code === 'aborted' ||
- code === 'failed-precondition' ||
- code === 'already-exists' ||
- !isPermanentError(code));
- }
- return false;
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const LOG_TAG$2 = 'FirestoreClient';
- const MAX_CONCURRENT_LIMBO_RESOLUTIONS = 100;
- /** DOMException error code constants. */
- const DOM_EXCEPTION_INVALID_STATE = 11;
- const DOM_EXCEPTION_ABORTED = 20;
- const DOM_EXCEPTION_QUOTA_EXCEEDED = 22;
- /**
- * FirestoreClient is a top-level class that constructs and owns all of the //
- * pieces of the client SDK architecture. It is responsible for creating the //
- * async queue that is shared by all of the other components in the system. //
- */
- class FirestoreClient {
- constructor(authCredentials, appCheckCredentials,
- /**
- * Asynchronous queue responsible for all of our internal processing. When
- * we get incoming work from the user (via public API) or the network
- * (incoming GRPC messages), we should always schedule onto this queue.
- * This ensures all of our work is properly serialized (e.g. we don't
- * start processing a new operation while the previous one is waiting for
- * an async I/O to complete).
- */
- asyncQueue, databaseInfo) {
- this.authCredentials = authCredentials;
- this.appCheckCredentials = appCheckCredentials;
- this.asyncQueue = asyncQueue;
- this.databaseInfo = databaseInfo;
- this.user = User.UNAUTHENTICATED;
- this.clientId = AutoId.newId();
- this.authCredentialListener = () => Promise.resolve();
- this.appCheckCredentialListener = () => Promise.resolve();
- this.authCredentials.start(asyncQueue, async (user) => {
- logDebug(LOG_TAG$2, 'Received user=', user.uid);
- await this.authCredentialListener(user);
- this.user = user;
- });
- this.appCheckCredentials.start(asyncQueue, newAppCheckToken => {
- logDebug(LOG_TAG$2, 'Received new app check token=', newAppCheckToken);
- return this.appCheckCredentialListener(newAppCheckToken, this.user);
- });
- }
- async getConfiguration() {
- return {
- asyncQueue: this.asyncQueue,
- databaseInfo: this.databaseInfo,
- clientId: this.clientId,
- authCredentials: this.authCredentials,
- appCheckCredentials: this.appCheckCredentials,
- initialUser: this.user,
- maxConcurrentLimboResolutions: MAX_CONCURRENT_LIMBO_RESOLUTIONS
- };
- }
- setCredentialChangeListener(listener) {
- this.authCredentialListener = listener;
- }
- setAppCheckTokenChangeListener(listener) {
- this.appCheckCredentialListener = listener;
- }
- /**
- * Checks that the client has not been terminated. Ensures that other methods on //
- * this class cannot be called after the client is terminated. //
- */
- verifyNotTerminated() {
- if (this.asyncQueue.isShuttingDown) {
- throw new FirestoreError(Code.FAILED_PRECONDITION, 'The client has already been terminated.');
- }
- }
- terminate() {
- this.asyncQueue.enterRestrictedMode();
- const deferred = new Deferred();
- this.asyncQueue.enqueueAndForgetEvenWhileRestricted(async () => {
- try {
- if (this._onlineComponents) {
- await this._onlineComponents.terminate();
- }
- if (this._offlineComponents) {
- await this._offlineComponents.terminate();
- }
- // The credentials provider must be terminated after shutting down the
- // RemoteStore as it will prevent the RemoteStore from retrieving auth
- // tokens.
- this.authCredentials.shutdown();
- this.appCheckCredentials.shutdown();
- deferred.resolve();
- }
- catch (e) {
- const firestoreError = wrapInUserErrorIfRecoverable(e, `Failed to shutdown persistence`);
- deferred.reject(firestoreError);
- }
- });
- return deferred.promise;
- }
- }
- async function setOfflineComponentProvider(client, offlineComponentProvider) {
- client.asyncQueue.verifyOperationInProgress();
- logDebug(LOG_TAG$2, 'Initializing OfflineComponentProvider');
- const configuration = await client.getConfiguration();
- await offlineComponentProvider.initialize(configuration);
- let currentUser = configuration.initialUser;
- client.setCredentialChangeListener(async (user) => {
- if (!currentUser.isEqual(user)) {
- await localStoreHandleUserChange(offlineComponentProvider.localStore, user);
- currentUser = user;
- }
- });
- // When a user calls clearPersistence() in one client, all other clients
- // need to be terminated to allow the delete to succeed.
- offlineComponentProvider.persistence.setDatabaseDeletedListener(() => client.terminate());
- client._offlineComponents = offlineComponentProvider;
- }
- async function setOnlineComponentProvider(client, onlineComponentProvider) {
- client.asyncQueue.verifyOperationInProgress();
- const offlineComponentProvider = await ensureOfflineComponents(client);
- logDebug(LOG_TAG$2, 'Initializing OnlineComponentProvider');
- const configuration = await client.getConfiguration();
- await onlineComponentProvider.initialize(offlineComponentProvider, configuration);
- // The CredentialChangeListener of the online component provider takes
- // precedence over the offline component provider.
- client.setCredentialChangeListener(user => remoteStoreHandleCredentialChange(onlineComponentProvider.remoteStore, user));
- client.setAppCheckTokenChangeListener((_, user) => remoteStoreHandleCredentialChange(onlineComponentProvider.remoteStore, user));
- client._onlineComponents = onlineComponentProvider;
- }
- /**
- * Decides whether the provided error allows us to gracefully disable
- * persistence (as opposed to crashing the client).
- */
- function canFallbackFromIndexedDbError(error) {
- if (error.name === 'FirebaseError') {
- return (error.code === Code.FAILED_PRECONDITION ||
- error.code === Code.UNIMPLEMENTED);
- }
- else if (typeof DOMException !== 'undefined' &&
- error instanceof DOMException) {
- // There are a few known circumstances where we can open IndexedDb but
- // trying to read/write will fail (e.g. quota exceeded). For
- // well-understood cases, we attempt to detect these and then gracefully
- // fall back to memory persistence.
- // NOTE: Rather than continue to add to this list, we could decide to
- // always fall back, with the risk that we might accidentally hide errors
- // representing actual SDK bugs.
- return (
- // When the browser is out of quota we could get either quota exceeded
- // or an aborted error depending on whether the error happened during
- // schema migration.
- error.code === DOM_EXCEPTION_QUOTA_EXCEEDED ||
- error.code === DOM_EXCEPTION_ABORTED ||
- // Firefox Private Browsing mode disables IndexedDb and returns
- // INVALID_STATE for any usage.
- error.code === DOM_EXCEPTION_INVALID_STATE);
- }
- return true;
- }
- async function ensureOfflineComponents(client) {
- if (!client._offlineComponents) {
- if (client._uninitializedComponentsProvider) {
- logDebug(LOG_TAG$2, 'Using user provided OfflineComponentProvider');
- try {
- await setOfflineComponentProvider(client, client._uninitializedComponentsProvider._offline);
- }
- catch (e) {
- const error = e;
- if (!canFallbackFromIndexedDbError(error)) {
- throw error;
- }
- logWarn('Error using user provided cache. Falling back to ' +
- 'memory cache: ' +
- error);
- await setOfflineComponentProvider(client, new MemoryOfflineComponentProvider());
- }
- }
- else {
- logDebug(LOG_TAG$2, 'Using default OfflineComponentProvider');
- await setOfflineComponentProvider(client, new MemoryOfflineComponentProvider());
- }
- }
- return client._offlineComponents;
- }
- async function ensureOnlineComponents(client) {
- if (!client._onlineComponents) {
- if (client._uninitializedComponentsProvider) {
- logDebug(LOG_TAG$2, 'Using user provided OnlineComponentProvider');
- await setOnlineComponentProvider(client, client._uninitializedComponentsProvider._online);
- }
- else {
- logDebug(LOG_TAG$2, 'Using default OnlineComponentProvider');
- await setOnlineComponentProvider(client, new OnlineComponentProvider());
- }
- }
- return client._onlineComponents;
- }
- function getPersistence(client) {
- return ensureOfflineComponents(client).then(c => c.persistence);
- }
- function getLocalStore(client) {
- return ensureOfflineComponents(client).then(c => c.localStore);
- }
- function getRemoteStore(client) {
- return ensureOnlineComponents(client).then(c => c.remoteStore);
- }
- function getSyncEngine(client) {
- return ensureOnlineComponents(client).then(c => c.syncEngine);
- }
- function getDatastore(client) {
- return ensureOnlineComponents(client).then(c => c.datastore);
- }
- async function getEventManager(client) {
- const onlineComponentProvider = await ensureOnlineComponents(client);
- const eventManager = onlineComponentProvider.eventManager;
- eventManager.onListen = syncEngineListen.bind(null, onlineComponentProvider.syncEngine);
- eventManager.onUnlisten = syncEngineUnlisten.bind(null, onlineComponentProvider.syncEngine);
- return eventManager;
- }
- /** Enables the network connection and re-enqueues all pending operations. */
- function firestoreClientEnableNetwork(client) {
- return client.asyncQueue.enqueue(async () => {
- const persistence = await getPersistence(client);
- const remoteStore = await getRemoteStore(client);
- persistence.setNetworkEnabled(true);
- return remoteStoreEnableNetwork(remoteStore);
- });
- }
- /** Disables the network connection. Pending operations will not complete. */
- function firestoreClientDisableNetwork(client) {
- return client.asyncQueue.enqueue(async () => {
- const persistence = await getPersistence(client);
- const remoteStore = await getRemoteStore(client);
- persistence.setNetworkEnabled(false);
- return remoteStoreDisableNetwork(remoteStore);
- });
- }
- /**
- * Returns a Promise that resolves when all writes that were pending at the time
- * this method was called received server acknowledgement. An acknowledgement
- * can be either acceptance or rejection.
- */
- function firestoreClientWaitForPendingWrites(client) {
- const deferred = new Deferred();
- client.asyncQueue.enqueueAndForget(async () => {
- const syncEngine = await getSyncEngine(client);
- return syncEngineRegisterPendingWritesCallback(syncEngine, deferred);
- });
- return deferred.promise;
- }
- function firestoreClientListen(client, query, options, observer) {
- const wrappedObserver = new AsyncObserver(observer);
- const listener = new QueryListener(query, wrappedObserver, options);
- client.asyncQueue.enqueueAndForget(async () => {
- const eventManager = await getEventManager(client);
- return eventManagerListen(eventManager, listener);
- });
- return () => {
- wrappedObserver.mute();
- client.asyncQueue.enqueueAndForget(async () => {
- const eventManager = await getEventManager(client);
- return eventManagerUnlisten(eventManager, listener);
- });
- };
- }
- function firestoreClientGetDocumentFromLocalCache(client, docKey) {
- const deferred = new Deferred();
- client.asyncQueue.enqueueAndForget(async () => {
- const localStore = await getLocalStore(client);
- return readDocumentFromCache(localStore, docKey, deferred);
- });
- return deferred.promise;
- }
- function firestoreClientGetDocumentViaSnapshotListener(client, key, options = {}) {
- const deferred = new Deferred();
- client.asyncQueue.enqueueAndForget(async () => {
- const eventManager = await getEventManager(client);
- return readDocumentViaSnapshotListener(eventManager, client.asyncQueue, key, options, deferred);
- });
- return deferred.promise;
- }
- function firestoreClientGetDocumentsFromLocalCache(client, query) {
- const deferred = new Deferred();
- client.asyncQueue.enqueueAndForget(async () => {
- const localStore = await getLocalStore(client);
- return executeQueryFromCache(localStore, query, deferred);
- });
- return deferred.promise;
- }
- function firestoreClientGetDocumentsViaSnapshotListener(client, query, options = {}) {
- const deferred = new Deferred();
- client.asyncQueue.enqueueAndForget(async () => {
- const eventManager = await getEventManager(client);
- return executeQueryViaSnapshotListener(eventManager, client.asyncQueue, query, options, deferred);
- });
- return deferred.promise;
- }
- function firestoreClientRunAggregateQuery(client, query, aggregates) {
- const deferred = new Deferred();
- client.asyncQueue.enqueueAndForget(async () => {
- // TODO (sum/avg) should we update this to use the event manager?
- // Implement and call executeAggregateQueryViaSnapshotListener, similar
- // to the implementation in firestoreClientGetDocumentsViaSnapshotListener
- // above
- try {
- // TODO(b/277628384): check `canUseNetwork()` and handle multi-tab.
- const datastore = await getDatastore(client);
- deferred.resolve(invokeRunAggregationQueryRpc(datastore, query, aggregates));
- }
- catch (e) {
- deferred.reject(e);
- }
- });
- return deferred.promise;
- }
- function firestoreClientWrite(client, mutations) {
- const deferred = new Deferred();
- client.asyncQueue.enqueueAndForget(async () => {
- const syncEngine = await getSyncEngine(client);
- return syncEngineWrite(syncEngine, mutations, deferred);
- });
- return deferred.promise;
- }
- function firestoreClientAddSnapshotsInSyncListener(client, observer) {
- const wrappedObserver = new AsyncObserver(observer);
- client.asyncQueue.enqueueAndForget(async () => {
- const eventManager = await getEventManager(client);
- return addSnapshotsInSyncListener(eventManager, wrappedObserver);
- });
- return () => {
- wrappedObserver.mute();
- client.asyncQueue.enqueueAndForget(async () => {
- const eventManager = await getEventManager(client);
- return removeSnapshotsInSyncListener(eventManager, wrappedObserver);
- });
- };
- }
- /**
- * Takes an updateFunction in which a set of reads and writes can be performed
- * atomically. In the updateFunction, the client can read and write values
- * using the supplied transaction object. After the updateFunction, all
- * changes will be committed. If a retryable error occurs (ex: some other
- * client has changed any of the data referenced), then the updateFunction
- * will be called again after a backoff. If the updateFunction still fails
- * after all retries, then the transaction will be rejected.
- *
- * The transaction object passed to the updateFunction contains methods for
- * accessing documents and collections. Unlike other datastore access, data
- * accessed with the transaction will not reflect local changes that have not
- * been committed. For this reason, it is required that all reads are
- * performed before any writes. Transactions must be performed while online.
- */
- function firestoreClientTransaction(client, updateFunction, options) {
- const deferred = new Deferred();
- client.asyncQueue.enqueueAndForget(async () => {
- const datastore = await getDatastore(client);
- new TransactionRunner(client.asyncQueue, datastore, options, updateFunction, deferred).run();
- });
- return deferred.promise;
- }
- async function readDocumentFromCache(localStore, docKey, result) {
- try {
- const document = await localStoreReadDocument(localStore, docKey);
- if (document.isFoundDocument()) {
- result.resolve(document);
- }
- else if (document.isNoDocument()) {
- result.resolve(null);
- }
- else {
- result.reject(new FirestoreError(Code.UNAVAILABLE, 'Failed to get document from cache. (However, this document may ' +
- "exist on the server. Run again without setting 'source' in " +
- 'the GetOptions to attempt to retrieve the document from the ' +
- 'server.)'));
- }
- }
- catch (e) {
- const firestoreError = wrapInUserErrorIfRecoverable(e, `Failed to get document '${docKey} from cache`);
- result.reject(firestoreError);
- }
- }
- /**
- * Retrieves a latency-compensated document from the backend via a
- * SnapshotListener.
- */
- function readDocumentViaSnapshotListener(eventManager, asyncQueue, key, options, result) {
- const wrappedObserver = new AsyncObserver({
- next: (snap) => {
- // Remove query first before passing event to user to avoid
- // user actions affecting the now stale query.
- asyncQueue.enqueueAndForget(() => eventManagerUnlisten(eventManager, listener));
- const exists = snap.docs.has(key);
- if (!exists && snap.fromCache) {
- // TODO(dimond): If we're online and the document doesn't
- // exist then we resolve with a doc.exists set to false. If
- // we're offline however, we reject the Promise in this
- // case. Two options: 1) Cache the negative response from
- // the server so we can deliver that even when you're
- // offline 2) Actually reject the Promise in the online case
- // if the document doesn't exist.
- result.reject(new FirestoreError(Code.UNAVAILABLE, 'Failed to get document because the client is offline.'));
- }
- else if (exists &&
- snap.fromCache &&
- options &&
- options.source === 'server') {
- result.reject(new FirestoreError(Code.UNAVAILABLE, 'Failed to get document from server. (However, this ' +
- 'document does exist in the local cache. Run again ' +
- 'without setting source to "server" to ' +
- 'retrieve the cached document.)'));
- }
- else {
- result.resolve(snap);
- }
- },
- error: e => result.reject(e)
- });
- const listener = new QueryListener(newQueryForPath(key.path), wrappedObserver, {
- includeMetadataChanges: true,
- waitForSyncWhenOnline: true
- });
- return eventManagerListen(eventManager, listener);
- }
- async function executeQueryFromCache(localStore, query, result) {
- try {
- const queryResult = await localStoreExecuteQuery(localStore, query,
- /* usePreviousResults= */ true);
- const view = new View(query, queryResult.remoteKeys);
- const viewDocChanges = view.computeDocChanges(queryResult.documents);
- const viewChange = view.applyChanges(viewDocChanges,
- /* updateLimboDocuments= */ false);
- result.resolve(viewChange.snapshot);
- }
- catch (e) {
- const firestoreError = wrapInUserErrorIfRecoverable(e, `Failed to execute query '${query} against cache`);
- result.reject(firestoreError);
- }
- }
- /**
- * Retrieves a latency-compensated query snapshot from the backend via a
- * SnapshotListener.
- */
- function executeQueryViaSnapshotListener(eventManager, asyncQueue, query, options, result) {
- const wrappedObserver = new AsyncObserver({
- next: snapshot => {
- // Remove query first before passing event to user to avoid
- // user actions affecting the now stale query.
- asyncQueue.enqueueAndForget(() => eventManagerUnlisten(eventManager, listener));
- if (snapshot.fromCache && options.source === 'server') {
- result.reject(new FirestoreError(Code.UNAVAILABLE, 'Failed to get documents from server. (However, these ' +
- 'documents may exist in the local cache. Run again ' +
- 'without setting source to "server" to ' +
- 'retrieve the cached documents.)'));
- }
- else {
- result.resolve(snapshot);
- }
- },
- error: e => result.reject(e)
- });
- const listener = new QueryListener(query, wrappedObserver, {
- includeMetadataChanges: true,
- waitForSyncWhenOnline: true
- });
- return eventManagerListen(eventManager, listener);
- }
- function firestoreClientLoadBundle(client, databaseId, data, resultTask) {
- const reader = createBundleReader(data, newSerializer(databaseId));
- client.asyncQueue.enqueueAndForget(async () => {
- syncEngineLoadBundle(await getSyncEngine(client), reader, resultTask);
- });
- }
- function firestoreClientGetNamedQuery(client, queryName) {
- return client.asyncQueue.enqueue(async () => localStoreGetNamedQuery(await getLocalStore(client), queryName));
- }
- function createBundleReader(data, serializer) {
- let content;
- if (typeof data === 'string') {
- content = newTextEncoder().encode(data);
- }
- else {
- content = data;
- }
- return newBundleReader(toByteStreamReader(content), serializer);
- }
- function firestoreClientSetIndexConfiguration(client, indexes) {
- return client.asyncQueue.enqueue(async () => {
- return localStoreConfigureFieldIndexes(await getLocalStore(client), indexes);
- });
- }
- /**
- * @license
- * Copyright 2023 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Compares two `ExperimentalLongPollingOptions` objects for equality.
- */
- function longPollingOptionsEqual(options1, options2) {
- return options1.timeoutSeconds === options2.timeoutSeconds;
- }
- /**
- * Creates and returns a new `ExperimentalLongPollingOptions` with the same
- * option values as the given instance.
- */
- function cloneLongPollingOptions(options) {
- const clone = {};
- if (options.timeoutSeconds !== undefined) {
- clone.timeoutSeconds = options.timeoutSeconds;
- }
- return clone;
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const LOG_TAG$1 = 'ComponentProvider';
- /**
- * An instance map that ensures only one Datastore exists per Firestore
- * instance.
- */
- const datastoreInstances = new Map();
- /**
- * Removes all components associated with the provided instance. Must be called
- * when the `Firestore` instance is terminated.
- */
- function removeComponents(firestore) {
- const datastore = datastoreInstances.get(firestore);
- if (datastore) {
- logDebug(LOG_TAG$1, 'Removing Datastore');
- datastoreInstances.delete(firestore);
- datastore.terminate();
- }
- }
- function makeDatabaseInfo(databaseId, appId, persistenceKey, settings) {
- return new DatabaseInfo(databaseId, appId, persistenceKey, settings.host, settings.ssl, settings.experimentalForceLongPolling, settings.experimentalAutoDetectLongPolling, cloneLongPollingOptions(settings.experimentalLongPollingOptions), settings.useFetchStreams);
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- // settings() defaults:
- const DEFAULT_HOST = 'firestore.googleapis.com';
- const DEFAULT_SSL = true;
- // The minimum long-polling timeout is hardcoded on the server. The value here
- // should be kept in sync with the value used by the server, as the server will
- // silently ignore a value below the minimum and fall back to the default.
- // Googlers see b/266868871 for relevant discussion.
- const MIN_LONG_POLLING_TIMEOUT_SECONDS = 5;
- // No maximum long-polling timeout is configured in the server, and defaults to
- // 30 seconds, which is what Watch appears to use.
- // Googlers see b/266868871 for relevant discussion.
- const MAX_LONG_POLLING_TIMEOUT_SECONDS = 30;
- // Whether long-polling auto-detected is enabled by default.
- const DEFAULT_AUTO_DETECT_LONG_POLLING = true;
- /**
- * A concrete type describing all the values that can be applied via a
- * user-supplied `FirestoreSettings` object. This is a separate type so that
- * defaults can be supplied and the value can be checked for equality.
- */
- class FirestoreSettingsImpl {
- constructor(settings) {
- var _a, _b;
- if (settings.host === undefined) {
- if (settings.ssl !== undefined) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, "Can't provide ssl option if host option is not set");
- }
- this.host = DEFAULT_HOST;
- this.ssl = DEFAULT_SSL;
- }
- else {
- this.host = settings.host;
- this.ssl = (_a = settings.ssl) !== null && _a !== void 0 ? _a : DEFAULT_SSL;
- }
- this.credentials = settings.credentials;
- this.ignoreUndefinedProperties = !!settings.ignoreUndefinedProperties;
- this.cache = settings.localCache;
- if (settings.cacheSizeBytes === undefined) {
- this.cacheSizeBytes = LRU_DEFAULT_CACHE_SIZE_BYTES;
- }
- else {
- if (settings.cacheSizeBytes !== LRU_COLLECTION_DISABLED &&
- settings.cacheSizeBytes < LRU_MINIMUM_CACHE_SIZE_BYTES) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `cacheSizeBytes must be at least ${LRU_MINIMUM_CACHE_SIZE_BYTES}`);
- }
- else {
- this.cacheSizeBytes = settings.cacheSizeBytes;
- }
- }
- validateIsNotUsedTogether('experimentalForceLongPolling', settings.experimentalForceLongPolling, 'experimentalAutoDetectLongPolling', settings.experimentalAutoDetectLongPolling);
- this.experimentalForceLongPolling = !!settings.experimentalForceLongPolling;
- if (this.experimentalForceLongPolling) {
- this.experimentalAutoDetectLongPolling = false;
- }
- else if (settings.experimentalAutoDetectLongPolling === undefined) {
- this.experimentalAutoDetectLongPolling = DEFAULT_AUTO_DETECT_LONG_POLLING;
- }
- else {
- // For backwards compatibility, coerce the value to boolean even though
- // the TypeScript compiler has narrowed the type to boolean already.
- // noinspection PointlessBooleanExpressionJS
- this.experimentalAutoDetectLongPolling =
- !!settings.experimentalAutoDetectLongPolling;
- }
- this.experimentalLongPollingOptions = cloneLongPollingOptions((_b = settings.experimentalLongPollingOptions) !== null && _b !== void 0 ? _b : {});
- validateLongPollingOptions(this.experimentalLongPollingOptions);
- this.useFetchStreams = !!settings.useFetchStreams;
- }
- isEqual(other) {
- return (this.host === other.host &&
- this.ssl === other.ssl &&
- this.credentials === other.credentials &&
- this.cacheSizeBytes === other.cacheSizeBytes &&
- this.experimentalForceLongPolling ===
- other.experimentalForceLongPolling &&
- this.experimentalAutoDetectLongPolling ===
- other.experimentalAutoDetectLongPolling &&
- longPollingOptionsEqual(this.experimentalLongPollingOptions, other.experimentalLongPollingOptions) &&
- this.ignoreUndefinedProperties === other.ignoreUndefinedProperties &&
- this.useFetchStreams === other.useFetchStreams);
- }
- }
- function validateLongPollingOptions(options) {
- if (options.timeoutSeconds !== undefined) {
- if (isNaN(options.timeoutSeconds)) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `invalid long polling timeout: ` +
- `${options.timeoutSeconds} (must not be NaN)`);
- }
- if (options.timeoutSeconds < MIN_LONG_POLLING_TIMEOUT_SECONDS) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `invalid long polling timeout: ${options.timeoutSeconds} ` +
- `(minimum allowed value is ${MIN_LONG_POLLING_TIMEOUT_SECONDS})`);
- }
- if (options.timeoutSeconds > MAX_LONG_POLLING_TIMEOUT_SECONDS) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `invalid long polling timeout: ${options.timeoutSeconds} ` +
- `(maximum allowed value is ${MAX_LONG_POLLING_TIMEOUT_SECONDS})`);
- }
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * The Cloud Firestore service interface.
- *
- * Do not call this constructor directly. Instead, use {@link (getFirestore:1)}.
- */
- class Firestore$1 {
- /** @hideconstructor */
- constructor(_authCredentials, _appCheckCredentials, _databaseId, _app) {
- this._authCredentials = _authCredentials;
- this._appCheckCredentials = _appCheckCredentials;
- this._databaseId = _databaseId;
- this._app = _app;
- /**
- * Whether it's a Firestore or Firestore Lite instance.
- */
- this.type = 'firestore-lite';
- this._persistenceKey = '(lite)';
- this._settings = new FirestoreSettingsImpl({});
- this._settingsFrozen = false;
- }
- /**
- * The {@link @firebase/app#FirebaseApp} associated with this `Firestore` service
- * instance.
- */
- get app() {
- if (!this._app) {
- throw new FirestoreError(Code.FAILED_PRECONDITION, "Firestore was not initialized using the Firebase SDK. 'app' is " +
- 'not available');
- }
- return this._app;
- }
- get _initialized() {
- return this._settingsFrozen;
- }
- get _terminated() {
- return this._terminateTask !== undefined;
- }
- _setSettings(settings) {
- if (this._settingsFrozen) {
- throw new FirestoreError(Code.FAILED_PRECONDITION, 'Firestore has already been started and its settings can no longer ' +
- 'be changed. You can only modify settings before calling any other ' +
- 'methods on a Firestore object.');
- }
- this._settings = new FirestoreSettingsImpl(settings);
- if (settings.credentials !== undefined) {
- this._authCredentials = makeAuthCredentialsProvider(settings.credentials);
- }
- }
- _getSettings() {
- return this._settings;
- }
- _freezeSettings() {
- this._settingsFrozen = true;
- return this._settings;
- }
- _delete() {
- if (!this._terminateTask) {
- this._terminateTask = this._terminate();
- }
- return this._terminateTask;
- }
- /** Returns a JSON-serializable representation of this `Firestore` instance. */
- toJSON() {
- return {
- app: this._app,
- databaseId: this._databaseId,
- settings: this._settings
- };
- }
- /**
- * Terminates all components used by this client. Subclasses can override
- * this method to clean up their own dependencies, but must also call this
- * method.
- *
- * Only ever called once.
- */
- _terminate() {
- removeComponents(this);
- return Promise.resolve();
- }
- }
- /**
- * Modify this instance to communicate with the Cloud Firestore emulator.
- *
- * Note: This must be called before this instance has been used to do any
- * operations.
- *
- * @param firestore - The `Firestore` instance to configure to connect to the
- * emulator.
- * @param host - the emulator host (ex: localhost).
- * @param port - the emulator port (ex: 9000).
- * @param options.mockUserToken - the mock auth token to use for unit testing
- * Security Rules.
- */
- function connectFirestoreEmulator(firestore, host, port, options = {}) {
- var _a;
- firestore = cast(firestore, Firestore$1);
- const settings = firestore._getSettings();
- const newHostSetting = `${host}:${port}`;
- if (settings.host !== DEFAULT_HOST && settings.host !== newHostSetting) {
- logWarn('Host has been set in both settings() and connectFirestoreEmulator(), emulator host ' +
- 'will be used.');
- }
- firestore._setSettings(Object.assign(Object.assign({}, settings), { host: newHostSetting, ssl: false }));
- if (options.mockUserToken) {
- let token;
- let user;
- if (typeof options.mockUserToken === 'string') {
- token = options.mockUserToken;
- user = User.MOCK_USER;
- }
- else {
- // Let createMockUserToken validate first (catches common mistakes like
- // invalid field "uid" and missing field "sub" / "user_id".)
- token = util.createMockUserToken(options.mockUserToken, (_a = firestore._app) === null || _a === void 0 ? void 0 : _a.options.projectId);
- const uid = options.mockUserToken.sub || options.mockUserToken.user_id;
- if (!uid) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, "mockUserToken must contain 'sub' or 'user_id' field!");
- }
- user = new User(uid);
- }
- firestore._authCredentials = new EmulatorAuthCredentialsProvider(new OAuthToken(token, user));
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * A `DocumentReference` refers to a document location in a Firestore database
- * and can be used to write, read, or listen to the location. The document at
- * the referenced location may or may not exist.
- */
- class DocumentReference {
- /** @hideconstructor */
- constructor(firestore,
- /**
- * If provided, the `FirestoreDataConverter` associated with this instance.
- */
- converter, _key) {
- this.converter = converter;
- this._key = _key;
- /** The type of this Firestore reference. */
- this.type = 'document';
- this.firestore = firestore;
- }
- get _path() {
- return this._key.path;
- }
- /**
- * The document's identifier within its collection.
- */
- get id() {
- return this._key.path.lastSegment();
- }
- /**
- * A string representing the path of the referenced document (relative
- * to the root of the database).
- */
- get path() {
- return this._key.path.canonicalString();
- }
- /**
- * The collection this `DocumentReference` belongs to.
- */
- get parent() {
- return new CollectionReference(this.firestore, this.converter, this._key.path.popLast());
- }
- withConverter(converter) {
- return new DocumentReference(this.firestore, converter, this._key);
- }
- }
- /**
- * A `Query` refers to a query which you can read or listen to. You can also
- * construct refined `Query` objects by adding filters and ordering.
- */
- class Query {
- // This is the lite version of the Query class in the main SDK.
- /** @hideconstructor protected */
- constructor(firestore,
- /**
- * If provided, the `FirestoreDataConverter` associated with this instance.
- */
- converter, _query) {
- this.converter = converter;
- this._query = _query;
- /** The type of this Firestore reference. */
- this.type = 'query';
- this.firestore = firestore;
- }
- withConverter(converter) {
- return new Query(this.firestore, converter, this._query);
- }
- }
- /**
- * A `CollectionReference` object can be used for adding documents, getting
- * document references, and querying for documents (using {@link (query:1)}).
- */
- class CollectionReference extends Query {
- /** @hideconstructor */
- constructor(firestore, converter, _path) {
- super(firestore, converter, newQueryForPath(_path));
- this._path = _path;
- /** The type of this Firestore reference. */
- this.type = 'collection';
- }
- /** The collection's identifier. */
- get id() {
- return this._query.path.lastSegment();
- }
- /**
- * A string representing the path of the referenced collection (relative
- * to the root of the database).
- */
- get path() {
- return this._query.path.canonicalString();
- }
- /**
- * A reference to the containing `DocumentReference` if this is a
- * subcollection. If this isn't a subcollection, the reference is null.
- */
- get parent() {
- const parentPath = this._path.popLast();
- if (parentPath.isEmpty()) {
- return null;
- }
- else {
- return new DocumentReference(this.firestore,
- /* converter= */ null, new DocumentKey(parentPath));
- }
- }
- withConverter(converter) {
- return new CollectionReference(this.firestore, converter, this._path);
- }
- }
- function collection(parent, path, ...pathSegments) {
- parent = util.getModularInstance(parent);
- validateNonEmptyArgument('collection', 'path', path);
- if (parent instanceof Firestore$1) {
- const absolutePath = ResourcePath.fromString(path, ...pathSegments);
- validateCollectionPath(absolutePath);
- return new CollectionReference(parent, /* converter= */ null, absolutePath);
- }
- else {
- if (!(parent instanceof DocumentReference) &&
- !(parent instanceof CollectionReference)) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Expected first argument to collection() to be a CollectionReference, ' +
- 'a DocumentReference or FirebaseFirestore');
- }
- const absolutePath = parent._path.child(ResourcePath.fromString(path, ...pathSegments));
- validateCollectionPath(absolutePath);
- return new CollectionReference(parent.firestore,
- /* converter= */ null, absolutePath);
- }
- }
- // TODO(firestorelite): Consider using ErrorFactory -
- // https://github.com/firebase/firebase-js-sdk/blob/0131e1f/packages/util/src/errors.ts#L106
- /**
- * Creates and returns a new `Query` instance that includes all documents in the
- * database that are contained in a collection or subcollection with the
- * given `collectionId`.
- *
- * @param firestore - A reference to the root `Firestore` instance.
- * @param collectionId - Identifies the collections to query over. Every
- * collection or subcollection with this ID as the last segment of its path
- * will be included. Cannot contain a slash.
- * @returns The created `Query`.
- */
- function collectionGroup(firestore, collectionId) {
- firestore = cast(firestore, Firestore$1);
- validateNonEmptyArgument('collectionGroup', 'collection id', collectionId);
- if (collectionId.indexOf('/') >= 0) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid collection ID '${collectionId}' passed to function ` +
- `collectionGroup(). Collection IDs must not contain '/'.`);
- }
- return new Query(firestore,
- /* converter= */ null, newQueryForCollectionGroup(collectionId));
- }
- function doc(parent, path, ...pathSegments) {
- parent = util.getModularInstance(parent);
- // We allow omission of 'pathString' but explicitly prohibit passing in both
- // 'undefined' and 'null'.
- if (arguments.length === 1) {
- path = AutoId.newId();
- }
- validateNonEmptyArgument('doc', 'path', path);
- if (parent instanceof Firestore$1) {
- const absolutePath = ResourcePath.fromString(path, ...pathSegments);
- validateDocumentPath(absolutePath);
- return new DocumentReference(parent,
- /* converter= */ null, new DocumentKey(absolutePath));
- }
- else {
- if (!(parent instanceof DocumentReference) &&
- !(parent instanceof CollectionReference)) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Expected first argument to collection() to be a CollectionReference, ' +
- 'a DocumentReference or FirebaseFirestore');
- }
- const absolutePath = parent._path.child(ResourcePath.fromString(path, ...pathSegments));
- validateDocumentPath(absolutePath);
- return new DocumentReference(parent.firestore, parent instanceof CollectionReference ? parent.converter : null, new DocumentKey(absolutePath));
- }
- }
- /**
- * Returns true if the provided references are equal.
- *
- * @param left - A reference to compare.
- * @param right - A reference to compare.
- * @returns true if the references point to the same location in the same
- * Firestore database.
- */
- function refEqual(left, right) {
- left = util.getModularInstance(left);
- right = util.getModularInstance(right);
- if ((left instanceof DocumentReference ||
- left instanceof CollectionReference) &&
- (right instanceof DocumentReference || right instanceof CollectionReference)) {
- return (left.firestore === right.firestore &&
- left.path === right.path &&
- left.converter === right.converter);
- }
- return false;
- }
- /**
- * Returns true if the provided queries point to the same collection and apply
- * the same constraints.
- *
- * @param left - A `Query` to compare.
- * @param right - A `Query` to compare.
- * @returns true if the references point to the same location in the same
- * Firestore database.
- */
- function queryEqual(left, right) {
- left = util.getModularInstance(left);
- right = util.getModularInstance(right);
- if (left instanceof Query && right instanceof Query) {
- return (left.firestore === right.firestore &&
- queryEquals(left._query, right._query) &&
- left.converter === right.converter);
- }
- return false;
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const LOG_TAG = 'AsyncQueue';
- class AsyncQueueImpl {
- constructor() {
- // The last promise in the queue.
- this.tail = Promise.resolve();
- // A list of retryable operations. Retryable operations are run in order and
- // retried with backoff.
- this.retryableOps = [];
- // Is this AsyncQueue being shut down? Once it is set to true, it will not
- // be changed again.
- this._isShuttingDown = false;
- // Operations scheduled to be queued in the future. Operations are
- // automatically removed after they are run or canceled.
- this.delayedOperations = [];
- // visible for testing
- this.failure = null;
- // Flag set while there's an outstanding AsyncQueue operation, used for
- // assertion sanity-checks.
- this.operationInProgress = false;
- // Enabled during shutdown on Safari to prevent future access to IndexedDB.
- this.skipNonRestrictedTasks = false;
- // List of TimerIds to fast-forward delays for.
- this.timerIdsToSkip = [];
- // Backoff timer used to schedule retries for retryable operations
- this.backoff = new ExponentialBackoff(this, "async_queue_retry" /* TimerId.AsyncQueueRetry */);
- // Visibility handler that triggers an immediate retry of all retryable
- // operations. Meant to speed up recovery when we regain file system access
- // after page comes into foreground.
- this.visibilityHandler = () => {
- this.backoff.skipBackoff();
- };
- }
- get isShuttingDown() {
- return this._isShuttingDown;
- }
- /**
- * Adds a new operation to the queue without waiting for it to complete (i.e.
- * we ignore the Promise result).
- */
- enqueueAndForget(op) {
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.enqueue(op);
- }
- enqueueAndForgetEvenWhileRestricted(op) {
- this.verifyNotFailed();
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.enqueueInternal(op);
- }
- enterRestrictedMode(purgeExistingTasks) {
- if (!this._isShuttingDown) {
- this._isShuttingDown = true;
- this.skipNonRestrictedTasks = purgeExistingTasks || false;
- }
- }
- enqueue(op) {
- this.verifyNotFailed();
- if (this._isShuttingDown) {
- // Return a Promise which never resolves.
- return new Promise(() => { });
- }
- // Create a deferred Promise that we can return to the callee. This
- // allows us to return a "hanging Promise" only to the callee and still
- // advance the queue even when the operation is not run.
- const task = new Deferred();
- return this.enqueueInternal(() => {
- if (this._isShuttingDown && this.skipNonRestrictedTasks) {
- // We do not resolve 'task'
- return Promise.resolve();
- }
- op().then(task.resolve, task.reject);
- return task.promise;
- }).then(() => task.promise);
- }
- enqueueRetryable(op) {
- this.enqueueAndForget(() => {
- this.retryableOps.push(op);
- return this.retryNextOp();
- });
- }
- /**
- * Runs the next operation from the retryable queue. If the operation fails,
- * reschedules with backoff.
- */
- async retryNextOp() {
- if (this.retryableOps.length === 0) {
- return;
- }
- try {
- await this.retryableOps[0]();
- this.retryableOps.shift();
- this.backoff.reset();
- }
- catch (e) {
- if (isIndexedDbTransactionError(e)) {
- logDebug(LOG_TAG, 'Operation failed with retryable error: ' + e);
- }
- else {
- throw e; // Failure will be handled by AsyncQueue
- }
- }
- if (this.retryableOps.length > 0) {
- // If there are additional operations, we re-schedule `retryNextOp()`.
- // This is necessary to run retryable operations that failed during
- // their initial attempt since we don't know whether they are already
- // enqueued. If, for example, `op1`, `op2`, `op3` are enqueued and `op1`
- // needs to be re-run, we will run `op1`, `op1`, `op2` using the
- // already enqueued calls to `retryNextOp()`. `op3()` will then run in the
- // call scheduled here.
- // Since `backoffAndRun()` cancels an existing backoff and schedules a
- // new backoff on every call, there is only ever a single additional
- // operation in the queue.
- this.backoff.backoffAndRun(() => this.retryNextOp());
- }
- }
- enqueueInternal(op) {
- const newTail = this.tail.then(() => {
- this.operationInProgress = true;
- return op()
- .catch((error) => {
- this.failure = error;
- this.operationInProgress = false;
- const message = getMessageOrStack(error);
- logError('INTERNAL UNHANDLED ERROR: ', message);
- // Re-throw the error so that this.tail becomes a rejected Promise and
- // all further attempts to chain (via .then) will just short-circuit
- // and return the rejected Promise.
- throw error;
- })
- .then(result => {
- this.operationInProgress = false;
- return result;
- });
- });
- this.tail = newTail;
- return newTail;
- }
- enqueueAfterDelay(timerId, delayMs, op) {
- this.verifyNotFailed();
- // Fast-forward delays for timerIds that have been overriden.
- if (this.timerIdsToSkip.indexOf(timerId) > -1) {
- delayMs = 0;
- }
- const delayedOp = DelayedOperation.createAndSchedule(this, timerId, delayMs, op, removedOp => this.removeDelayedOperation(removedOp));
- this.delayedOperations.push(delayedOp);
- return delayedOp;
- }
- verifyNotFailed() {
- if (this.failure) {
- fail();
- }
- }
- verifyOperationInProgress() {
- }
- /**
- * Waits until all currently queued tasks are finished executing. Delayed
- * operations are not run.
- */
- async drain() {
- // Operations in the queue prior to draining may have enqueued additional
- // operations. Keep draining the queue until the tail is no longer advanced,
- // which indicates that no more new operations were enqueued and that all
- // operations were executed.
- let currentTail;
- do {
- currentTail = this.tail;
- await currentTail;
- } while (currentTail !== this.tail);
- }
- /**
- * For Tests: Determine if a delayed operation with a particular TimerId
- * exists.
- */
- containsDelayedOperation(timerId) {
- for (const op of this.delayedOperations) {
- if (op.timerId === timerId) {
- return true;
- }
- }
- return false;
- }
- /**
- * For Tests: Runs some or all delayed operations early.
- *
- * @param lastTimerId - Delayed operations up to and including this TimerId
- * will be drained. Pass TimerId.All to run all delayed operations.
- * @returns a Promise that resolves once all operations have been run.
- */
- runAllDelayedOperationsUntil(lastTimerId) {
- // Note that draining may generate more delayed ops, so we do that first.
- return this.drain().then(() => {
- // Run ops in the same order they'd run if they ran naturally.
- this.delayedOperations.sort((a, b) => a.targetTimeMs - b.targetTimeMs);
- for (const op of this.delayedOperations) {
- op.skipDelay();
- if (lastTimerId !== "all" /* TimerId.All */ && op.timerId === lastTimerId) {
- break;
- }
- }
- return this.drain();
- });
- }
- /**
- * For Tests: Skip all subsequent delays for a timer id.
- */
- skipDelaysForTimerId(timerId) {
- this.timerIdsToSkip.push(timerId);
- }
- /** Called once a DelayedOperation is run or canceled. */
- removeDelayedOperation(op) {
- // NOTE: indexOf / slice are O(n), but delayedOperations is expected to be small.
- const index = this.delayedOperations.indexOf(op);
- this.delayedOperations.splice(index, 1);
- }
- }
- function newAsyncQueue() {
- return new AsyncQueueImpl();
- }
- /**
- * Chrome includes Error.message in Error.stack. Other browsers do not.
- * This returns expected output of message + stack when available.
- * @param error - Error or FirestoreError
- */
- function getMessageOrStack(error) {
- let message = error.message || '';
- if (error.stack) {
- if (error.stack.includes(error.message)) {
- message = error.stack;
- }
- else {
- message = error.message + '\n' + error.stack;
- }
- }
- return message;
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Represents the task of loading a Firestore bundle. It provides progress of bundle
- * loading, as well as task completion and error events.
- *
- * The API is compatible with `Promise<LoadBundleTaskProgress>`.
- */
- class LoadBundleTask {
- constructor() {
- this._progressObserver = {};
- this._taskCompletionResolver = new Deferred();
- this._lastProgress = {
- taskState: 'Running',
- totalBytes: 0,
- totalDocuments: 0,
- bytesLoaded: 0,
- documentsLoaded: 0
- };
- }
- /**
- * Registers functions to listen to bundle loading progress events.
- * @param next - Called when there is a progress update from bundle loading. Typically `next` calls occur
- * each time a Firestore document is loaded from the bundle.
- * @param error - Called when an error occurs during bundle loading. The task aborts after reporting the
- * error, and there should be no more updates after this.
- * @param complete - Called when the loading task is complete.
- */
- onProgress(next, error, complete) {
- this._progressObserver = {
- next,
- error,
- complete
- };
- }
- /**
- * Implements the `Promise<LoadBundleTaskProgress>.catch` interface.
- *
- * @param onRejected - Called when an error occurs during bundle loading.
- */
- catch(onRejected) {
- return this._taskCompletionResolver.promise.catch(onRejected);
- }
- /**
- * Implements the `Promise<LoadBundleTaskProgress>.then` interface.
- *
- * @param onFulfilled - Called on the completion of the loading task with a final `LoadBundleTaskProgress` update.
- * The update will always have its `taskState` set to `"Success"`.
- * @param onRejected - Called when an error occurs during bundle loading.
- */
- then(onFulfilled, onRejected) {
- return this._taskCompletionResolver.promise.then(onFulfilled, onRejected);
- }
- /**
- * Notifies all observers that bundle loading has completed, with a provided
- * `LoadBundleTaskProgress` object.
- *
- * @private
- */
- _completeWith(progress) {
- this._updateProgress(progress);
- if (this._progressObserver.complete) {
- this._progressObserver.complete();
- }
- this._taskCompletionResolver.resolve(progress);
- }
- /**
- * Notifies all observers that bundle loading has failed, with a provided
- * `Error` as the reason.
- *
- * @private
- */
- _failWith(error) {
- this._lastProgress.taskState = 'Error';
- if (this._progressObserver.next) {
- this._progressObserver.next(this._lastProgress);
- }
- if (this._progressObserver.error) {
- this._progressObserver.error(error);
- }
- this._taskCompletionResolver.reject(error);
- }
- /**
- * Notifies a progress update of loading a bundle.
- * @param progress - The new progress.
- *
- * @private
- */
- _updateProgress(progress) {
- this._lastProgress = progress;
- if (this._progressObserver.next) {
- this._progressObserver.next(progress);
- }
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Constant used to indicate the LRU garbage collection should be disabled.
- * Set this value as the `cacheSizeBytes` on the settings passed to the
- * {@link Firestore} instance.
- */
- const CACHE_SIZE_UNLIMITED = LRU_COLLECTION_DISABLED;
- /**
- * The Cloud Firestore service interface.
- *
- * Do not call this constructor directly. Instead, use {@link (getFirestore:1)}.
- */
- class Firestore extends Firestore$1 {
- /** @hideconstructor */
- constructor(authCredentialsProvider, appCheckCredentialsProvider, databaseId, app) {
- super(authCredentialsProvider, appCheckCredentialsProvider, databaseId, app);
- /**
- * Whether it's a {@link Firestore} or Firestore Lite instance.
- */
- this.type = 'firestore';
- this._queue = newAsyncQueue();
- this._persistenceKey = (app === null || app === void 0 ? void 0 : app.name) || '[DEFAULT]';
- }
- _terminate() {
- if (!this._firestoreClient) {
- // The client must be initialized to ensure that all subsequent API
- // usage throws an exception.
- configureFirestore(this);
- }
- return this._firestoreClient.terminate();
- }
- }
- /**
- * Initializes a new instance of {@link Firestore} with the provided settings.
- * Can only be called before any other function, including
- * {@link (getFirestore:1)}. If the custom settings are empty, this function is
- * equivalent to calling {@link (getFirestore:1)}.
- *
- * @param app - The {@link @firebase/app#FirebaseApp} with which the {@link Firestore} instance will
- * be associated.
- * @param settings - A settings object to configure the {@link Firestore} instance.
- * @param databaseId - The name of the database.
- * @returns A newly initialized {@link Firestore} instance.
- */
- function initializeFirestore(app$1, settings, databaseId) {
- if (!databaseId) {
- databaseId = DEFAULT_DATABASE_NAME;
- }
- const provider = app._getProvider(app$1, 'firestore');
- if (provider.isInitialized(databaseId)) {
- const existingInstance = provider.getImmediate({
- identifier: databaseId
- });
- const initialSettings = provider.getOptions(databaseId);
- if (util.deepEqual(initialSettings, settings)) {
- return existingInstance;
- }
- else {
- throw new FirestoreError(Code.FAILED_PRECONDITION, 'initializeFirestore() has already been called with ' +
- 'different options. To avoid this error, call initializeFirestore() with the ' +
- 'same options as when it was originally called, or call getFirestore() to return the' +
- ' already initialized instance.');
- }
- }
- if (settings.cacheSizeBytes !== undefined &&
- settings.localCache !== undefined) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `cache and cacheSizeBytes cannot be specified at the same time as cacheSizeBytes will` +
- `be deprecated. Instead, specify the cache size in the cache object`);
- }
- if (settings.cacheSizeBytes !== undefined &&
- settings.cacheSizeBytes !== CACHE_SIZE_UNLIMITED &&
- settings.cacheSizeBytes < LRU_MINIMUM_CACHE_SIZE_BYTES) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `cacheSizeBytes must be at least ${LRU_MINIMUM_CACHE_SIZE_BYTES}`);
- }
- return provider.initialize({
- options: settings,
- instanceIdentifier: databaseId
- });
- }
- function getFirestore(appOrDatabaseId, optionalDatabaseId) {
- const app$1 = typeof appOrDatabaseId === 'object' ? appOrDatabaseId : app.getApp();
- const databaseId = typeof appOrDatabaseId === 'string'
- ? appOrDatabaseId
- : optionalDatabaseId || DEFAULT_DATABASE_NAME;
- const db = app._getProvider(app$1, 'firestore').getImmediate({
- identifier: databaseId
- });
- if (!db._initialized) {
- const emulator = util.getDefaultEmulatorHostnameAndPort('firestore');
- if (emulator) {
- connectFirestoreEmulator(db, ...emulator);
- }
- }
- return db;
- }
- /**
- * @internal
- */
- function ensureFirestoreConfigured(firestore) {
- if (!firestore._firestoreClient) {
- configureFirestore(firestore);
- }
- firestore._firestoreClient.verifyNotTerminated();
- return firestore._firestoreClient;
- }
- function configureFirestore(firestore) {
- var _a, _b, _c;
- const settings = firestore._freezeSettings();
- const databaseInfo = makeDatabaseInfo(firestore._databaseId, ((_a = firestore._app) === null || _a === void 0 ? void 0 : _a.options.appId) || '', firestore._persistenceKey, settings);
- firestore._firestoreClient = new FirestoreClient(firestore._authCredentials, firestore._appCheckCredentials, firestore._queue, databaseInfo);
- if (((_b = settings.cache) === null || _b === void 0 ? void 0 : _b._offlineComponentProvider) &&
- ((_c = settings.cache) === null || _c === void 0 ? void 0 : _c._onlineComponentProvider)) {
- firestore._firestoreClient._uninitializedComponentsProvider = {
- _offlineKind: settings.cache.kind,
- _offline: settings.cache._offlineComponentProvider,
- _online: settings.cache._onlineComponentProvider
- };
- }
- }
- /**
- * Attempts to enable persistent storage, if possible.
- *
- * Must be called before any other functions (other than
- * {@link initializeFirestore}, {@link (getFirestore:1)} or
- * {@link clearIndexedDbPersistence}.
- *
- * If this fails, `enableIndexedDbPersistence()` will reject the promise it
- * returns. Note that even after this failure, the {@link Firestore} instance will
- * remain usable, however offline persistence will be disabled.
- *
- * There are several reasons why this can fail, which can be identified by
- * the `code` on the error.
- *
- * * failed-precondition: The app is already open in another browser tab.
- * * unimplemented: The browser is incompatible with the offline
- * persistence implementation.
- *
- * Persistence cannot be used in a Node.js environment.
- *
- * @param firestore - The {@link Firestore} instance to enable persistence for.
- * @param persistenceSettings - Optional settings object to configure
- * persistence.
- * @returns A `Promise` that represents successfully enabling persistent storage.
- * @deprecated This function will be removed in a future major release. Instead, set
- * `FirestoreSettings.cache` to an instance of `IndexedDbLocalCache` to
- * turn on IndexedDb cache. Calling this function when `FirestoreSettings.cache`
- * is already specified will throw an exception.
- */
- function enableIndexedDbPersistence(firestore, persistenceSettings) {
- firestore = cast(firestore, Firestore);
- verifyNotInitialized(firestore);
- const client = ensureFirestoreConfigured(firestore);
- if (client._uninitializedComponentsProvider) {
- throw new FirestoreError(Code.FAILED_PRECONDITION, 'SDK cache is already specified.');
- }
- logWarn('enableIndexedDbPersistence() will be deprecated in the future, ' +
- 'you can use `FirestoreSettings.cache` instead.');
- const settings = firestore._freezeSettings();
- const onlineComponentProvider = new OnlineComponentProvider();
- const offlineComponentProvider = new IndexedDbOfflineComponentProvider(onlineComponentProvider, settings.cacheSizeBytes, persistenceSettings === null || persistenceSettings === void 0 ? void 0 : persistenceSettings.forceOwnership);
- return setPersistenceProviders(client, onlineComponentProvider, offlineComponentProvider);
- }
- /**
- * Attempts to enable multi-tab persistent storage, if possible. If enabled
- * across all tabs, all operations share access to local persistence, including
- * shared execution of queries and latency-compensated local document updates
- * across all connected instances.
- *
- * If this fails, `enableMultiTabIndexedDbPersistence()` will reject the promise
- * it returns. Note that even after this failure, the {@link Firestore} instance will
- * remain usable, however offline persistence will be disabled.
- *
- * There are several reasons why this can fail, which can be identified by
- * the `code` on the error.
- *
- * * failed-precondition: The app is already open in another browser tab and
- * multi-tab is not enabled.
- * * unimplemented: The browser is incompatible with the offline
- * persistence implementation.
- *
- * @param firestore - The {@link Firestore} instance to enable persistence for.
- * @returns A `Promise` that represents successfully enabling persistent
- * storage.
- * @deprecated This function will be removed in a future major release. Instead, set
- * `FirestoreSettings.cache` to an instance of `IndexedDbLocalCache` to
- * turn on indexeddb cache. Calling this function when `FirestoreSettings.cache`
- * is already specified will throw an exception.
- */
- function enableMultiTabIndexedDbPersistence(firestore) {
- firestore = cast(firestore, Firestore);
- verifyNotInitialized(firestore);
- const client = ensureFirestoreConfigured(firestore);
- if (client._uninitializedComponentsProvider) {
- throw new FirestoreError(Code.FAILED_PRECONDITION, 'SDK cache is already specified.');
- }
- logWarn('enableMultiTabIndexedDbPersistence() will be deprecated in the future, ' +
- 'you can use `FirestoreSettings.cache` instead.');
- const settings = firestore._freezeSettings();
- const onlineComponentProvider = new OnlineComponentProvider();
- const offlineComponentProvider = new MultiTabOfflineComponentProvider(onlineComponentProvider, settings.cacheSizeBytes);
- return setPersistenceProviders(client, onlineComponentProvider, offlineComponentProvider);
- }
- /**
- * Registers both the `OfflineComponentProvider` and `OnlineComponentProvider`.
- * If the operation fails with a recoverable error (see
- * `canRecoverFromIndexedDbError()` below), the returned Promise is rejected
- * but the client remains usable.
- */
- function setPersistenceProviders(client, onlineComponentProvider, offlineComponentProvider) {
- const persistenceResult = new Deferred();
- return client.asyncQueue
- .enqueue(async () => {
- try {
- await setOfflineComponentProvider(client, offlineComponentProvider);
- await setOnlineComponentProvider(client, onlineComponentProvider);
- persistenceResult.resolve();
- }
- catch (e) {
- const error = e;
- if (!canFallbackFromIndexedDbError(error)) {
- throw error;
- }
- logWarn('Error enabling indexeddb cache. Falling back to ' +
- 'memory cache: ' +
- error);
- persistenceResult.reject(error);
- }
- })
- .then(() => persistenceResult.promise);
- }
- /**
- * Clears the persistent storage. This includes pending writes and cached
- * documents.
- *
- * Must be called while the {@link Firestore} instance is not started (after the app is
- * terminated or when the app is first initialized). On startup, this function
- * must be called before other functions (other than {@link
- * initializeFirestore} or {@link (getFirestore:1)})). If the {@link Firestore}
- * instance is still running, the promise will be rejected with the error code
- * of `failed-precondition`.
- *
- * Note: `clearIndexedDbPersistence()` is primarily intended to help write
- * reliable tests that use Cloud Firestore. It uses an efficient mechanism for
- * dropping existing data but does not attempt to securely overwrite or
- * otherwise make cached data unrecoverable. For applications that are sensitive
- * to the disclosure of cached data in between user sessions, we strongly
- * recommend not enabling persistence at all.
- *
- * @param firestore - The {@link Firestore} instance to clear persistence for.
- * @returns A `Promise` that is resolved when the persistent storage is
- * cleared. Otherwise, the promise is rejected with an error.
- */
- function clearIndexedDbPersistence(firestore) {
- if (firestore._initialized && !firestore._terminated) {
- throw new FirestoreError(Code.FAILED_PRECONDITION, 'Persistence can only be cleared before a Firestore instance is ' +
- 'initialized or after it is terminated.');
- }
- const deferred = new Deferred();
- firestore._queue.enqueueAndForgetEvenWhileRestricted(async () => {
- try {
- await indexedDbClearPersistence(indexedDbStoragePrefix(firestore._databaseId, firestore._persistenceKey));
- deferred.resolve();
- }
- catch (e) {
- deferred.reject(e);
- }
- });
- return deferred.promise;
- }
- /**
- * Waits until all currently pending writes for the active user have been
- * acknowledged by the backend.
- *
- * The returned promise resolves immediately if there are no outstanding writes.
- * Otherwise, the promise waits for all previously issued writes (including
- * those written in a previous app session), but it does not wait for writes
- * that were added after the function is called. If you want to wait for
- * additional writes, call `waitForPendingWrites()` again.
- *
- * Any outstanding `waitForPendingWrites()` promises are rejected during user
- * changes.
- *
- * @returns A `Promise` which resolves when all currently pending writes have been
- * acknowledged by the backend.
- */
- function waitForPendingWrites(firestore) {
- firestore = cast(firestore, Firestore);
- const client = ensureFirestoreConfigured(firestore);
- return firestoreClientWaitForPendingWrites(client);
- }
- /**
- * Re-enables use of the network for this {@link Firestore} instance after a prior
- * call to {@link disableNetwork}.
- *
- * @returns A `Promise` that is resolved once the network has been enabled.
- */
- function enableNetwork(firestore) {
- firestore = cast(firestore, Firestore);
- const client = ensureFirestoreConfigured(firestore);
- return firestoreClientEnableNetwork(client);
- }
- /**
- * Disables network usage for this instance. It can be re-enabled via {@link
- * enableNetwork}. While the network is disabled, any snapshot listeners,
- * `getDoc()` or `getDocs()` calls will return results from cache, and any write
- * operations will be queued until the network is restored.
- *
- * @returns A `Promise` that is resolved once the network has been disabled.
- */
- function disableNetwork(firestore) {
- firestore = cast(firestore, Firestore);
- const client = ensureFirestoreConfigured(firestore);
- return firestoreClientDisableNetwork(client);
- }
- /**
- * Terminates the provided {@link Firestore} instance.
- *
- * After calling `terminate()` only the `clearIndexedDbPersistence()` function
- * may be used. Any other function will throw a `FirestoreError`.
- *
- * To restart after termination, create a new instance of FirebaseFirestore with
- * {@link (getFirestore:1)}.
- *
- * Termination does not cancel any pending writes, and any promises that are
- * awaiting a response from the server will not be resolved. If you have
- * persistence enabled, the next time you start this instance, it will resume
- * sending these writes to the server.
- *
- * Note: Under normal circumstances, calling `terminate()` is not required. This
- * function is useful only when you want to force this instance to release all
- * of its resources or in combination with `clearIndexedDbPersistence()` to
- * ensure that all local state is destroyed between test runs.
- *
- * @returns A `Promise` that is resolved when the instance has been successfully
- * terminated.
- */
- function terminate(firestore) {
- app._removeServiceInstance(firestore.app, 'firestore', firestore._databaseId.database);
- return firestore._delete();
- }
- /**
- * Loads a Firestore bundle into the local cache.
- *
- * @param firestore - The {@link Firestore} instance to load bundles for.
- * @param bundleData - An object representing the bundle to be loaded. Valid
- * objects are `ArrayBuffer`, `ReadableStream<Uint8Array>` or `string`.
- *
- * @returns A `LoadBundleTask` object, which notifies callers with progress
- * updates, and completion or error events. It can be used as a
- * `Promise<LoadBundleTaskProgress>`.
- */
- function loadBundle(firestore, bundleData) {
- firestore = cast(firestore, Firestore);
- const client = ensureFirestoreConfigured(firestore);
- const resultTask = new LoadBundleTask();
- firestoreClientLoadBundle(client, firestore._databaseId, bundleData, resultTask);
- return resultTask;
- }
- /**
- * Reads a Firestore {@link Query} from local cache, identified by the given
- * name.
- *
- * The named queries are packaged into bundles on the server side (along
- * with resulting documents), and loaded to local cache using `loadBundle`. Once
- * in local cache, use this method to extract a {@link Query} by name.
- *
- * @param firestore - The {@link Firestore} instance to read the query from.
- * @param name - The name of the query.
- * @returns A `Promise` that is resolved with the Query or `null`.
- */
- function namedQuery(firestore, name) {
- firestore = cast(firestore, Firestore);
- const client = ensureFirestoreConfigured(firestore);
- return firestoreClientGetNamedQuery(client, name).then(namedQuery => {
- if (!namedQuery) {
- return null;
- }
- return new Query(firestore, null, namedQuery.query);
- });
- }
- function verifyNotInitialized(firestore) {
- if (firestore._initialized || firestore._terminated) {
- throw new FirestoreError(Code.FAILED_PRECONDITION, 'Firestore has already been started and persistence can no longer be ' +
- 'enabled. You can only enable persistence before calling any other ' +
- 'methods on a Firestore object.');
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- function registerFirestore(variant, useFetchStreams = true) {
- setSDKVersion(app.SDK_VERSION);
- app._registerComponent(new component.Component('firestore', (container, { instanceIdentifier: databaseId, options: settings }) => {
- const app = container.getProvider('app').getImmediate();
- const firestoreInstance = new Firestore(new FirebaseAuthCredentialsProvider(container.getProvider('auth-internal')), new FirebaseAppCheckTokenProvider(container.getProvider('app-check-internal')), databaseIdFromApp(app, databaseId), app);
- settings = Object.assign({ useFetchStreams }, settings);
- firestoreInstance._setSettings(settings);
- return firestoreInstance;
- }, 'PUBLIC').setMultipleInstances(true));
- app.registerVersion(name, version$1, variant);
- // BUILD_TARGET will be replaced by values like esm5, esm2017, cjs5, etc during the compilation
- app.registerVersion(name, version$1, 'cjs2017');
- }
- /**
- * @license
- * Copyright 2023 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Concrete implementation of the Aggregate type.
- */
- class AggregateImpl {
- constructor(alias, aggregateType, fieldPath) {
- this.alias = alias;
- this.aggregateType = aggregateType;
- this.fieldPath = fieldPath;
- }
- }
- /**
- * @license
- * Copyright 2022 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Represents an aggregation that can be performed by Firestore.
- */
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- class AggregateField {
- /**
- * Create a new AggregateField<T>
- * @param _aggregateType Specifies the type of aggregation operation to perform.
- * @param _internalFieldPath Optionally specifies the field that is aggregated.
- * @internal
- */
- constructor(
- // TODO (sum/avg) make aggregateType public when the feature is supported
- _aggregateType = 'count', _internalFieldPath) {
- this._aggregateType = _aggregateType;
- this._internalFieldPath = _internalFieldPath;
- /** A type string to uniquely identify instances of this class. */
- this.type = 'AggregateField';
- }
- }
- /**
- * The results of executing an aggregation query.
- */
- class AggregateQuerySnapshot {
- /** @hideconstructor */
- constructor(query, _userDataWriter, _data) {
- this._userDataWriter = _userDataWriter;
- this._data = _data;
- /** A type string to uniquely identify instances of this class. */
- this.type = 'AggregateQuerySnapshot';
- this.query = query;
- }
- /**
- * Returns the results of the aggregations performed over the underlying
- * query.
- *
- * The keys of the returned object will be the same as those of the
- * `AggregateSpec` object specified to the aggregation method, and the values
- * will be the corresponding aggregation result.
- *
- * @returns The results of the aggregations performed over the underlying
- * query.
- */
- data() {
- return this._userDataWriter.convertObjectMap(this._data);
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * An immutable object representing an array of bytes.
- */
- class Bytes {
- /** @hideconstructor */
- constructor(byteString) {
- this._byteString = byteString;
- }
- /**
- * Creates a new `Bytes` object from the given Base64 string, converting it to
- * bytes.
- *
- * @param base64 - The Base64 string used to create the `Bytes` object.
- */
- static fromBase64String(base64) {
- try {
- return new Bytes(ByteString.fromBase64String(base64));
- }
- catch (e) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Failed to construct data from Base64 string: ' + e);
- }
- }
- /**
- * Creates a new `Bytes` object from the given Uint8Array.
- *
- * @param array - The Uint8Array used to create the `Bytes` object.
- */
- static fromUint8Array(array) {
- return new Bytes(ByteString.fromUint8Array(array));
- }
- /**
- * Returns the underlying bytes as a Base64-encoded string.
- *
- * @returns The Base64-encoded string created from the `Bytes` object.
- */
- toBase64() {
- return this._byteString.toBase64();
- }
- /**
- * Returns the underlying bytes in a new `Uint8Array`.
- *
- * @returns The Uint8Array created from the `Bytes` object.
- */
- toUint8Array() {
- return this._byteString.toUint8Array();
- }
- /**
- * Returns a string representation of the `Bytes` object.
- *
- * @returns A string representation of the `Bytes` object.
- */
- toString() {
- return 'Bytes(base64: ' + this.toBase64() + ')';
- }
- /**
- * Returns true if this `Bytes` object is equal to the provided one.
- *
- * @param other - The `Bytes` object to compare against.
- * @returns true if this `Bytes` object is equal to the provided one.
- */
- isEqual(other) {
- return this._byteString.isEqual(other._byteString);
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * A `FieldPath` refers to a field in a document. The path may consist of a
- * single field name (referring to a top-level field in the document), or a
- * list of field names (referring to a nested field in the document).
- *
- * Create a `FieldPath` by providing field names. If more than one field
- * name is provided, the path will point to a nested field in a document.
- */
- class FieldPath {
- /**
- * Creates a `FieldPath` from the provided field names. If more than one field
- * name is provided, the path will point to a nested field in a document.
- *
- * @param fieldNames - A list of field names.
- */
- constructor(...fieldNames) {
- for (let i = 0; i < fieldNames.length; ++i) {
- if (fieldNames[i].length === 0) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid field name at argument $(i + 1). ` +
- 'Field names must not be empty.');
- }
- }
- this._internalPath = new FieldPath$1(fieldNames);
- }
- /**
- * Returns true if this `FieldPath` is equal to the provided one.
- *
- * @param other - The `FieldPath` to compare against.
- * @returns true if this `FieldPath` is equal to the provided one.
- */
- isEqual(other) {
- return this._internalPath.isEqual(other._internalPath);
- }
- }
- /**
- * Returns a special sentinel `FieldPath` to refer to the ID of a document.
- * It can be used in queries to sort or filter by the document ID.
- */
- function documentId() {
- return new FieldPath(DOCUMENT_KEY_NAME);
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Sentinel values that can be used when writing document fields with `set()`
- * or `update()`.
- */
- class FieldValue {
- /**
- * @param _methodName - The public API endpoint that returns this class.
- * @hideconstructor
- */
- constructor(_methodName) {
- this._methodName = _methodName;
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * An immutable object representing a geographic location in Firestore. The
- * location is represented as latitude/longitude pair.
- *
- * Latitude values are in the range of [-90, 90].
- * Longitude values are in the range of [-180, 180].
- */
- class GeoPoint {
- /**
- * Creates a new immutable `GeoPoint` object with the provided latitude and
- * longitude values.
- * @param latitude - The latitude as number between -90 and 90.
- * @param longitude - The longitude as number between -180 and 180.
- */
- constructor(latitude, longitude) {
- if (!isFinite(latitude) || latitude < -90 || latitude > 90) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Latitude must be a number between -90 and 90, but was: ' + latitude);
- }
- if (!isFinite(longitude) || longitude < -180 || longitude > 180) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Longitude must be a number between -180 and 180, but was: ' + longitude);
- }
- this._lat = latitude;
- this._long = longitude;
- }
- /**
- * The latitude of this `GeoPoint` instance.
- */
- get latitude() {
- return this._lat;
- }
- /**
- * The longitude of this `GeoPoint` instance.
- */
- get longitude() {
- return this._long;
- }
- /**
- * Returns true if this `GeoPoint` is equal to the provided one.
- *
- * @param other - The `GeoPoint` to compare against.
- * @returns true if this `GeoPoint` is equal to the provided one.
- */
- isEqual(other) {
- return this._lat === other._lat && this._long === other._long;
- }
- /** Returns a JSON-serializable representation of this GeoPoint. */
- toJSON() {
- return { latitude: this._lat, longitude: this._long };
- }
- /**
- * Actually private to JS consumers of our API, so this function is prefixed
- * with an underscore.
- */
- _compareTo(other) {
- return (primitiveComparator(this._lat, other._lat) ||
- primitiveComparator(this._long, other._long));
- }
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const RESERVED_FIELD_REGEX = /^__.*__$/;
- /** The result of parsing document data (e.g. for a setData call). */
- class ParsedSetData {
- constructor(data, fieldMask, fieldTransforms) {
- this.data = data;
- this.fieldMask = fieldMask;
- this.fieldTransforms = fieldTransforms;
- }
- toMutation(key, precondition) {
- if (this.fieldMask !== null) {
- return new PatchMutation(key, this.data, this.fieldMask, precondition, this.fieldTransforms);
- }
- else {
- return new SetMutation(key, this.data, precondition, this.fieldTransforms);
- }
- }
- }
- /** The result of parsing "update" data (i.e. for an updateData call). */
- class ParsedUpdateData {
- constructor(data,
- // The fieldMask does not include document transforms.
- fieldMask, fieldTransforms) {
- this.data = data;
- this.fieldMask = fieldMask;
- this.fieldTransforms = fieldTransforms;
- }
- toMutation(key, precondition) {
- return new PatchMutation(key, this.data, this.fieldMask, precondition, this.fieldTransforms);
- }
- }
- function isWrite(dataSource) {
- switch (dataSource) {
- case 0 /* UserDataSource.Set */: // fall through
- case 2 /* UserDataSource.MergeSet */: // fall through
- case 1 /* UserDataSource.Update */:
- return true;
- case 3 /* UserDataSource.Argument */:
- case 4 /* UserDataSource.ArrayArgument */:
- return false;
- default:
- throw fail();
- }
- }
- /** A "context" object passed around while parsing user data. */
- class ParseContextImpl {
- /**
- * Initializes a ParseContext with the given source and path.
- *
- * @param settings - The settings for the parser.
- * @param databaseId - The database ID of the Firestore instance.
- * @param serializer - The serializer to use to generate the Value proto.
- * @param ignoreUndefinedProperties - Whether to ignore undefined properties
- * rather than throw.
- * @param fieldTransforms - A mutable list of field transforms encountered
- * while parsing the data.
- * @param fieldMask - A mutable list of field paths encountered while parsing
- * the data.
- *
- * TODO(b/34871131): We don't support array paths right now, so path can be
- * null to indicate the context represents any location within an array (in
- * which case certain features will not work and errors will be somewhat
- * compromised).
- */
- constructor(settings, databaseId, serializer, ignoreUndefinedProperties, fieldTransforms, fieldMask) {
- this.settings = settings;
- this.databaseId = databaseId;
- this.serializer = serializer;
- this.ignoreUndefinedProperties = ignoreUndefinedProperties;
- // Minor hack: If fieldTransforms is undefined, we assume this is an
- // external call and we need to validate the entire path.
- if (fieldTransforms === undefined) {
- this.validatePath();
- }
- this.fieldTransforms = fieldTransforms || [];
- this.fieldMask = fieldMask || [];
- }
- get path() {
- return this.settings.path;
- }
- get dataSource() {
- return this.settings.dataSource;
- }
- /** Returns a new context with the specified settings overwritten. */
- contextWith(configuration) {
- return new ParseContextImpl(Object.assign(Object.assign({}, this.settings), configuration), this.databaseId, this.serializer, this.ignoreUndefinedProperties, this.fieldTransforms, this.fieldMask);
- }
- childContextForField(field) {
- var _a;
- const childPath = (_a = this.path) === null || _a === void 0 ? void 0 : _a.child(field);
- const context = this.contextWith({ path: childPath, arrayElement: false });
- context.validatePathSegment(field);
- return context;
- }
- childContextForFieldPath(field) {
- var _a;
- const childPath = (_a = this.path) === null || _a === void 0 ? void 0 : _a.child(field);
- const context = this.contextWith({ path: childPath, arrayElement: false });
- context.validatePath();
- return context;
- }
- childContextForArray(index) {
- // TODO(b/34871131): We don't support array paths right now; so make path
- // undefined.
- return this.contextWith({ path: undefined, arrayElement: true });
- }
- createError(reason) {
- return createError(reason, this.settings.methodName, this.settings.hasConverter || false, this.path, this.settings.targetDoc);
- }
- /** Returns 'true' if 'fieldPath' was traversed when creating this context. */
- contains(fieldPath) {
- return (this.fieldMask.find(field => fieldPath.isPrefixOf(field)) !== undefined ||
- this.fieldTransforms.find(transform => fieldPath.isPrefixOf(transform.field)) !== undefined);
- }
- validatePath() {
- // TODO(b/34871131): Remove null check once we have proper paths for fields
- // within arrays.
- if (!this.path) {
- return;
- }
- for (let i = 0; i < this.path.length; i++) {
- this.validatePathSegment(this.path.get(i));
- }
- }
- validatePathSegment(segment) {
- if (segment.length === 0) {
- throw this.createError('Document fields must not be empty');
- }
- if (isWrite(this.dataSource) && RESERVED_FIELD_REGEX.test(segment)) {
- throw this.createError('Document fields cannot begin and end with "__"');
- }
- }
- }
- /**
- * Helper for parsing raw user input (provided via the API) into internal model
- * classes.
- */
- class UserDataReader {
- constructor(databaseId, ignoreUndefinedProperties, serializer) {
- this.databaseId = databaseId;
- this.ignoreUndefinedProperties = ignoreUndefinedProperties;
- this.serializer = serializer || newSerializer(databaseId);
- }
- /** Creates a new top-level parse context. */
- createContext(dataSource, methodName, targetDoc, hasConverter = false) {
- return new ParseContextImpl({
- dataSource,
- methodName,
- targetDoc,
- path: FieldPath$1.emptyPath(),
- arrayElement: false,
- hasConverter
- }, this.databaseId, this.serializer, this.ignoreUndefinedProperties);
- }
- }
- function newUserDataReader(firestore) {
- const settings = firestore._freezeSettings();
- const serializer = newSerializer(firestore._databaseId);
- return new UserDataReader(firestore._databaseId, !!settings.ignoreUndefinedProperties, serializer);
- }
- /** Parse document data from a set() call. */
- function parseSetData(userDataReader, methodName, targetDoc, input, hasConverter, options = {}) {
- const context = userDataReader.createContext(options.merge || options.mergeFields
- ? 2 /* UserDataSource.MergeSet */
- : 0 /* UserDataSource.Set */, methodName, targetDoc, hasConverter);
- validatePlainObject('Data must be an object, but it was:', context, input);
- const updateData = parseObject(input, context);
- let fieldMask;
- let fieldTransforms;
- if (options.merge) {
- fieldMask = new FieldMask(context.fieldMask);
- fieldTransforms = context.fieldTransforms;
- }
- else if (options.mergeFields) {
- const validatedFieldPaths = [];
- for (const stringOrFieldPath of options.mergeFields) {
- const fieldPath = fieldPathFromArgument$1(methodName, stringOrFieldPath, targetDoc);
- if (!context.contains(fieldPath)) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Field '${fieldPath}' is specified in your field mask but missing from your input data.`);
- }
- if (!fieldMaskContains(validatedFieldPaths, fieldPath)) {
- validatedFieldPaths.push(fieldPath);
- }
- }
- fieldMask = new FieldMask(validatedFieldPaths);
- fieldTransforms = context.fieldTransforms.filter(transform => fieldMask.covers(transform.field));
- }
- else {
- fieldMask = null;
- fieldTransforms = context.fieldTransforms;
- }
- return new ParsedSetData(new ObjectValue(updateData), fieldMask, fieldTransforms);
- }
- class DeleteFieldValueImpl extends FieldValue {
- _toFieldTransform(context) {
- if (context.dataSource === 2 /* UserDataSource.MergeSet */) {
- // No transform to add for a delete, but we need to add it to our
- // fieldMask so it gets deleted.
- context.fieldMask.push(context.path);
- }
- else if (context.dataSource === 1 /* UserDataSource.Update */) {
- throw context.createError(`${this._methodName}() can only appear at the top level ` +
- 'of your update data');
- }
- else {
- // We shouldn't encounter delete sentinels for queries or non-merge set() calls.
- throw context.createError(`${this._methodName}() cannot be used with set() unless you pass ` +
- '{merge:true}');
- }
- return null;
- }
- isEqual(other) {
- return other instanceof DeleteFieldValueImpl;
- }
- }
- /**
- * Creates a child context for parsing SerializableFieldValues.
- *
- * This is different than calling `ParseContext.contextWith` because it keeps
- * the fieldTransforms and fieldMask separate.
- *
- * The created context has its `dataSource` set to `UserDataSource.Argument`.
- * Although these values are used with writes, any elements in these FieldValues
- * are not considered writes since they cannot contain any FieldValue sentinels,
- * etc.
- *
- * @param fieldValue - The sentinel FieldValue for which to create a child
- * context.
- * @param context - The parent context.
- * @param arrayElement - Whether or not the FieldValue has an array.
- */
- function createSentinelChildContext(fieldValue, context, arrayElement) {
- return new ParseContextImpl({
- dataSource: 3 /* UserDataSource.Argument */,
- targetDoc: context.settings.targetDoc,
- methodName: fieldValue._methodName,
- arrayElement
- }, context.databaseId, context.serializer, context.ignoreUndefinedProperties);
- }
- class ServerTimestampFieldValueImpl extends FieldValue {
- _toFieldTransform(context) {
- return new FieldTransform(context.path, new ServerTimestampTransform());
- }
- isEqual(other) {
- return other instanceof ServerTimestampFieldValueImpl;
- }
- }
- class ArrayUnionFieldValueImpl extends FieldValue {
- constructor(methodName, _elements) {
- super(methodName);
- this._elements = _elements;
- }
- _toFieldTransform(context) {
- const parseContext = createSentinelChildContext(this, context,
- /*array=*/ true);
- const parsedElements = this._elements.map(element => parseData(element, parseContext));
- const arrayUnion = new ArrayUnionTransformOperation(parsedElements);
- return new FieldTransform(context.path, arrayUnion);
- }
- isEqual(other) {
- // TODO(mrschmidt): Implement isEquals
- return this === other;
- }
- }
- class ArrayRemoveFieldValueImpl extends FieldValue {
- constructor(methodName, _elements) {
- super(methodName);
- this._elements = _elements;
- }
- _toFieldTransform(context) {
- const parseContext = createSentinelChildContext(this, context,
- /*array=*/ true);
- const parsedElements = this._elements.map(element => parseData(element, parseContext));
- const arrayUnion = new ArrayRemoveTransformOperation(parsedElements);
- return new FieldTransform(context.path, arrayUnion);
- }
- isEqual(other) {
- // TODO(mrschmidt): Implement isEquals
- return this === other;
- }
- }
- class NumericIncrementFieldValueImpl extends FieldValue {
- constructor(methodName, _operand) {
- super(methodName);
- this._operand = _operand;
- }
- _toFieldTransform(context) {
- const numericIncrement = new NumericIncrementTransformOperation(context.serializer, toNumber(context.serializer, this._operand));
- return new FieldTransform(context.path, numericIncrement);
- }
- isEqual(other) {
- // TODO(mrschmidt): Implement isEquals
- return this === other;
- }
- }
- /** Parse update data from an update() call. */
- function parseUpdateData(userDataReader, methodName, targetDoc, input) {
- const context = userDataReader.createContext(1 /* UserDataSource.Update */, methodName, targetDoc);
- validatePlainObject('Data must be an object, but it was:', context, input);
- const fieldMaskPaths = [];
- const updateData = ObjectValue.empty();
- forEach(input, (key, value) => {
- const path = fieldPathFromDotSeparatedString(methodName, key, targetDoc);
- // For Compat types, we have to "extract" the underlying types before
- // performing validation.
- value = util.getModularInstance(value);
- const childContext = context.childContextForFieldPath(path);
- if (value instanceof DeleteFieldValueImpl) {
- // Add it to the field mask, but don't add anything to updateData.
- fieldMaskPaths.push(path);
- }
- else {
- const parsedValue = parseData(value, childContext);
- if (parsedValue != null) {
- fieldMaskPaths.push(path);
- updateData.set(path, parsedValue);
- }
- }
- });
- const mask = new FieldMask(fieldMaskPaths);
- return new ParsedUpdateData(updateData, mask, context.fieldTransforms);
- }
- /** Parse update data from a list of field/value arguments. */
- function parseUpdateVarargs(userDataReader, methodName, targetDoc, field, value, moreFieldsAndValues) {
- const context = userDataReader.createContext(1 /* UserDataSource.Update */, methodName, targetDoc);
- const keys = [fieldPathFromArgument$1(methodName, field, targetDoc)];
- const values = [value];
- if (moreFieldsAndValues.length % 2 !== 0) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Function ${methodName}() needs to be called with an even number ` +
- 'of arguments that alternate between field names and values.');
- }
- for (let i = 0; i < moreFieldsAndValues.length; i += 2) {
- keys.push(fieldPathFromArgument$1(methodName, moreFieldsAndValues[i]));
- values.push(moreFieldsAndValues[i + 1]);
- }
- const fieldMaskPaths = [];
- const updateData = ObjectValue.empty();
- // We iterate in reverse order to pick the last value for a field if the
- // user specified the field multiple times.
- for (let i = keys.length - 1; i >= 0; --i) {
- if (!fieldMaskContains(fieldMaskPaths, keys[i])) {
- const path = keys[i];
- let value = values[i];
- // For Compat types, we have to "extract" the underlying types before
- // performing validation.
- value = util.getModularInstance(value);
- const childContext = context.childContextForFieldPath(path);
- if (value instanceof DeleteFieldValueImpl) {
- // Add it to the field mask, but don't add anything to updateData.
- fieldMaskPaths.push(path);
- }
- else {
- const parsedValue = parseData(value, childContext);
- if (parsedValue != null) {
- fieldMaskPaths.push(path);
- updateData.set(path, parsedValue);
- }
- }
- }
- }
- const mask = new FieldMask(fieldMaskPaths);
- return new ParsedUpdateData(updateData, mask, context.fieldTransforms);
- }
- /**
- * Parse a "query value" (e.g. value in a where filter or a value in a cursor
- * bound).
- *
- * @param allowArrays - Whether the query value is an array that may directly
- * contain additional arrays (e.g. the operand of an `in` query).
- */
- function parseQueryValue(userDataReader, methodName, input, allowArrays = false) {
- const context = userDataReader.createContext(allowArrays ? 4 /* UserDataSource.ArrayArgument */ : 3 /* UserDataSource.Argument */, methodName);
- const parsed = parseData(input, context);
- return parsed;
- }
- /**
- * Parses user data to Protobuf Values.
- *
- * @param input - Data to be parsed.
- * @param context - A context object representing the current path being parsed,
- * the source of the data being parsed, etc.
- * @returns The parsed value, or null if the value was a FieldValue sentinel
- * that should not be included in the resulting parsed data.
- */
- function parseData(input, context) {
- // Unwrap the API type from the Compat SDK. This will return the API type
- // from firestore-exp.
- input = util.getModularInstance(input);
- if (looksLikeJsonObject(input)) {
- validatePlainObject('Unsupported field value:', context, input);
- return parseObject(input, context);
- }
- else if (input instanceof FieldValue) {
- // FieldValues usually parse into transforms (except deleteField())
- // in which case we do not want to include this field in our parsed data
- // (as doing so will overwrite the field directly prior to the transform
- // trying to transform it). So we don't add this location to
- // context.fieldMask and we return null as our parsing result.
- parseSentinelFieldValue(input, context);
- return null;
- }
- else if (input === undefined && context.ignoreUndefinedProperties) {
- // If the input is undefined it can never participate in the fieldMask, so
- // don't handle this below. If `ignoreUndefinedProperties` is false,
- // `parseScalarValue` will reject an undefined value.
- return null;
- }
- else {
- // If context.path is null we are inside an array and we don't support
- // field mask paths more granular than the top-level array.
- if (context.path) {
- context.fieldMask.push(context.path);
- }
- if (input instanceof Array) {
- // TODO(b/34871131): Include the path containing the array in the error
- // message.
- // In the case of IN queries, the parsed data is an array (representing
- // the set of values to be included for the IN query) that may directly
- // contain additional arrays (each representing an individual field
- // value), so we disable this validation.
- if (context.settings.arrayElement &&
- context.dataSource !== 4 /* UserDataSource.ArrayArgument */) {
- throw context.createError('Nested arrays are not supported');
- }
- return parseArray(input, context);
- }
- else {
- return parseScalarValue(input, context);
- }
- }
- }
- function parseObject(obj, context) {
- const fields = {};
- if (isEmpty(obj)) {
- // If we encounter an empty object, we explicitly add it to the update
- // mask to ensure that the server creates a map entry.
- if (context.path && context.path.length > 0) {
- context.fieldMask.push(context.path);
- }
- }
- else {
- forEach(obj, (key, val) => {
- const parsedValue = parseData(val, context.childContextForField(key));
- if (parsedValue != null) {
- fields[key] = parsedValue;
- }
- });
- }
- return { mapValue: { fields } };
- }
- function parseArray(array, context) {
- const values = [];
- let entryIndex = 0;
- for (const entry of array) {
- let parsedEntry = parseData(entry, context.childContextForArray(entryIndex));
- if (parsedEntry == null) {
- // Just include nulls in the array for fields being replaced with a
- // sentinel.
- parsedEntry = { nullValue: 'NULL_VALUE' };
- }
- values.push(parsedEntry);
- entryIndex++;
- }
- return { arrayValue: { values } };
- }
- /**
- * "Parses" the provided FieldValueImpl, adding any necessary transforms to
- * context.fieldTransforms.
- */
- function parseSentinelFieldValue(value, context) {
- // Sentinels are only supported with writes, and not within arrays.
- if (!isWrite(context.dataSource)) {
- throw context.createError(`${value._methodName}() can only be used with update() and set()`);
- }
- if (!context.path) {
- throw context.createError(`${value._methodName}() is not currently supported inside arrays`);
- }
- const fieldTransform = value._toFieldTransform(context);
- if (fieldTransform) {
- context.fieldTransforms.push(fieldTransform);
- }
- }
- /**
- * Helper to parse a scalar value (i.e. not an Object, Array, or FieldValue)
- *
- * @returns The parsed value
- */
- function parseScalarValue(value, context) {
- value = util.getModularInstance(value);
- if (value === null) {
- return { nullValue: 'NULL_VALUE' };
- }
- else if (typeof value === 'number') {
- return toNumber(context.serializer, value);
- }
- else if (typeof value === 'boolean') {
- return { booleanValue: value };
- }
- else if (typeof value === 'string') {
- return { stringValue: value };
- }
- else if (value instanceof Date) {
- const timestamp = Timestamp.fromDate(value);
- return {
- timestampValue: toTimestamp(context.serializer, timestamp)
- };
- }
- else if (value instanceof Timestamp) {
- // Firestore backend truncates precision down to microseconds. To ensure
- // offline mode works the same with regards to truncation, perform the
- // truncation immediately without waiting for the backend to do that.
- const timestamp = new Timestamp(value.seconds, Math.floor(value.nanoseconds / 1000) * 1000);
- return {
- timestampValue: toTimestamp(context.serializer, timestamp)
- };
- }
- else if (value instanceof GeoPoint) {
- return {
- geoPointValue: {
- latitude: value.latitude,
- longitude: value.longitude
- }
- };
- }
- else if (value instanceof Bytes) {
- return { bytesValue: toBytes(context.serializer, value._byteString) };
- }
- else if (value instanceof DocumentReference) {
- const thisDb = context.databaseId;
- const otherDb = value.firestore._databaseId;
- if (!otherDb.isEqual(thisDb)) {
- throw context.createError('Document reference is for database ' +
- `${otherDb.projectId}/${otherDb.database} but should be ` +
- `for database ${thisDb.projectId}/${thisDb.database}`);
- }
- return {
- referenceValue: toResourceName(value.firestore._databaseId || context.databaseId, value._key.path)
- };
- }
- else {
- throw context.createError(`Unsupported field value: ${valueDescription(value)}`);
- }
- }
- /**
- * Checks whether an object looks like a JSON object that should be converted
- * into a struct. Normal class/prototype instances are considered to look like
- * JSON objects since they should be converted to a struct value. Arrays, Dates,
- * GeoPoints, etc. are not considered to look like JSON objects since they map
- * to specific FieldValue types other than ObjectValue.
- */
- function looksLikeJsonObject(input) {
- return (typeof input === 'object' &&
- input !== null &&
- !(input instanceof Array) &&
- !(input instanceof Date) &&
- !(input instanceof Timestamp) &&
- !(input instanceof GeoPoint) &&
- !(input instanceof Bytes) &&
- !(input instanceof DocumentReference) &&
- !(input instanceof FieldValue));
- }
- function validatePlainObject(message, context, input) {
- if (!looksLikeJsonObject(input) || !isPlainObject(input)) {
- const description = valueDescription(input);
- if (description === 'an object') {
- // Massage the error if it was an object.
- throw context.createError(message + ' a custom object');
- }
- else {
- throw context.createError(message + ' ' + description);
- }
- }
- }
- /**
- * Helper that calls fromDotSeparatedString() but wraps any error thrown.
- */
- function fieldPathFromArgument$1(methodName, path, targetDoc) {
- // If required, replace the FieldPath Compat class with with the firestore-exp
- // FieldPath.
- path = util.getModularInstance(path);
- if (path instanceof FieldPath) {
- return path._internalPath;
- }
- else if (typeof path === 'string') {
- return fieldPathFromDotSeparatedString(methodName, path);
- }
- else {
- const message = 'Field path arguments must be of type string or ';
- throw createError(message, methodName,
- /* hasConverter= */ false,
- /* path= */ undefined, targetDoc);
- }
- }
- /**
- * Matches any characters in a field path string that are reserved.
- */
- const FIELD_PATH_RESERVED = new RegExp('[~\\*/\\[\\]]');
- /**
- * Wraps fromDotSeparatedString with an error message about the method that
- * was thrown.
- * @param methodName - The publicly visible method name
- * @param path - The dot-separated string form of a field path which will be
- * split on dots.
- * @param targetDoc - The document against which the field path will be
- * evaluated.
- */
- function fieldPathFromDotSeparatedString(methodName, path, targetDoc) {
- const found = path.search(FIELD_PATH_RESERVED);
- if (found >= 0) {
- throw createError(`Invalid field path (${path}). Paths must not contain ` +
- `'~', '*', '/', '[', or ']'`, methodName,
- /* hasConverter= */ false,
- /* path= */ undefined, targetDoc);
- }
- try {
- return new FieldPath(...path.split('.'))._internalPath;
- }
- catch (e) {
- throw createError(`Invalid field path (${path}). Paths must not be empty, ` +
- `begin with '.', end with '.', or contain '..'`, methodName,
- /* hasConverter= */ false,
- /* path= */ undefined, targetDoc);
- }
- }
- function createError(reason, methodName, hasConverter, path, targetDoc) {
- const hasPath = path && !path.isEmpty();
- const hasDocument = targetDoc !== undefined;
- let message = `Function ${methodName}() called with invalid data`;
- if (hasConverter) {
- message += ' (via `toFirestore()`)';
- }
- message += '. ';
- let description = '';
- if (hasPath || hasDocument) {
- description += ' (found';
- if (hasPath) {
- description += ` in field ${path}`;
- }
- if (hasDocument) {
- description += ` in document ${targetDoc}`;
- }
- description += ')';
- }
- return new FirestoreError(Code.INVALID_ARGUMENT, message + reason + description);
- }
- /** Checks `haystack` if FieldPath `needle` is present. Runs in O(n). */
- function fieldMaskContains(haystack, needle) {
- return haystack.some(v => v.isEqual(needle));
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * A `DocumentSnapshot` contains data read from a document in your Firestore
- * database. The data can be extracted with `.data()` or `.get(<field>)` to
- * get a specific field.
- *
- * For a `DocumentSnapshot` that points to a non-existing document, any data
- * access will return 'undefined'. You can use the `exists()` method to
- * explicitly verify a document's existence.
- */
- class DocumentSnapshot$1 {
- // Note: This class is stripped down version of the DocumentSnapshot in
- // the legacy SDK. The changes are:
- // - No support for SnapshotMetadata.
- // - No support for SnapshotOptions.
- /** @hideconstructor protected */
- constructor(_firestore, _userDataWriter, _key, _document, _converter) {
- this._firestore = _firestore;
- this._userDataWriter = _userDataWriter;
- this._key = _key;
- this._document = _document;
- this._converter = _converter;
- }
- /** Property of the `DocumentSnapshot` that provides the document's ID. */
- get id() {
- return this._key.path.lastSegment();
- }
- /**
- * The `DocumentReference` for the document included in the `DocumentSnapshot`.
- */
- get ref() {
- return new DocumentReference(this._firestore, this._converter, this._key);
- }
- /**
- * Signals whether or not the document at the snapshot's location exists.
- *
- * @returns true if the document exists.
- */
- exists() {
- return this._document !== null;
- }
- /**
- * Retrieves all fields in the document as an `Object`. Returns `undefined` if
- * the document doesn't exist.
- *
- * @returns An `Object` containing all fields in the document or `undefined`
- * if the document doesn't exist.
- */
- data() {
- if (!this._document) {
- return undefined;
- }
- else if (this._converter) {
- // We only want to use the converter and create a new DocumentSnapshot
- // if a converter has been provided.
- const snapshot = new QueryDocumentSnapshot$1(this._firestore, this._userDataWriter, this._key, this._document,
- /* converter= */ null);
- return this._converter.fromFirestore(snapshot);
- }
- else {
- return this._userDataWriter.convertValue(this._document.data.value);
- }
- }
- /**
- * Retrieves the field specified by `fieldPath`. Returns `undefined` if the
- * document or field doesn't exist.
- *
- * @param fieldPath - The path (for example 'foo' or 'foo.bar') to a specific
- * field.
- * @returns The data at the specified field location or undefined if no such
- * field exists in the document.
- */
- // We are using `any` here to avoid an explicit cast by our users.
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- get(fieldPath) {
- if (this._document) {
- const value = this._document.data.field(fieldPathFromArgument('DocumentSnapshot.get', fieldPath));
- if (value !== null) {
- return this._userDataWriter.convertValue(value);
- }
- }
- return undefined;
- }
- }
- /**
- * A `QueryDocumentSnapshot` contains data read from a document in your
- * Firestore database as part of a query. The document is guaranteed to exist
- * and its data can be extracted with `.data()` or `.get(<field>)` to get a
- * specific field.
- *
- * A `QueryDocumentSnapshot` offers the same API surface as a
- * `DocumentSnapshot`. Since query results contain only existing documents, the
- * `exists` property will always be true and `data()` will never return
- * 'undefined'.
- */
- class QueryDocumentSnapshot$1 extends DocumentSnapshot$1 {
- /**
- * Retrieves all fields in the document as an `Object`.
- *
- * @override
- * @returns An `Object` containing all fields in the document.
- */
- data() {
- return super.data();
- }
- }
- /**
- * Helper that calls `fromDotSeparatedString()` but wraps any error thrown.
- */
- function fieldPathFromArgument(methodName, arg) {
- if (typeof arg === 'string') {
- return fieldPathFromDotSeparatedString(methodName, arg);
- }
- else if (arg instanceof FieldPath) {
- return arg._internalPath;
- }
- else {
- return arg._delegate._internalPath;
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- function validateHasExplicitOrderByForLimitToLast(query) {
- if (query.limitType === "L" /* LimitType.Last */ &&
- query.explicitOrderBy.length === 0) {
- throw new FirestoreError(Code.UNIMPLEMENTED, 'limitToLast() queries require specifying at least one orderBy() clause');
- }
- }
- /**
- * An `AppliableConstraint` is an abstraction of a constraint that can be applied
- * to a Firestore query.
- */
- class AppliableConstraint {
- }
- /**
- * A `QueryConstraint` is used to narrow the set of documents returned by a
- * Firestore query. `QueryConstraint`s are created by invoking {@link where},
- * {@link orderBy}, {@link (startAt:1)}, {@link (startAfter:1)}, {@link
- * (endBefore:1)}, {@link (endAt:1)}, {@link limit}, {@link limitToLast} and
- * can then be passed to {@link (query:1)} to create a new query instance that
- * also contains this `QueryConstraint`.
- */
- class QueryConstraint extends AppliableConstraint {
- }
- function query(query, queryConstraint, ...additionalQueryConstraints) {
- let queryConstraints = [];
- if (queryConstraint instanceof AppliableConstraint) {
- queryConstraints.push(queryConstraint);
- }
- queryConstraints = queryConstraints.concat(additionalQueryConstraints);
- validateQueryConstraintArray(queryConstraints);
- for (const constraint of queryConstraints) {
- query = constraint._apply(query);
- }
- return query;
- }
- /**
- * A `QueryFieldFilterConstraint` is used to narrow the set of documents returned by
- * a Firestore query by filtering on one or more document fields.
- * `QueryFieldFilterConstraint`s are created by invoking {@link where} and can then
- * be passed to {@link (query:1)} to create a new query instance that also contains
- * this `QueryFieldFilterConstraint`.
- */
- class QueryFieldFilterConstraint extends QueryConstraint {
- /**
- * @internal
- */
- constructor(_field, _op, _value) {
- super();
- this._field = _field;
- this._op = _op;
- this._value = _value;
- /** The type of this query constraint */
- this.type = 'where';
- }
- static _create(_field, _op, _value) {
- return new QueryFieldFilterConstraint(_field, _op, _value);
- }
- _apply(query) {
- const filter = this._parse(query);
- validateNewFieldFilter(query._query, filter);
- return new Query(query.firestore, query.converter, queryWithAddedFilter(query._query, filter));
- }
- _parse(query) {
- const reader = newUserDataReader(query.firestore);
- const filter = newQueryFilter(query._query, 'where', reader, query.firestore._databaseId, this._field, this._op, this._value);
- return filter;
- }
- }
- /**
- * Creates a {@link QueryFieldFilterConstraint} that enforces that documents
- * must contain the specified field and that the value should satisfy the
- * relation constraint provided.
- *
- * @param fieldPath - The path to compare
- * @param opStr - The operation string (e.g "<", "<=", "==", "<",
- * "<=", "!=").
- * @param value - The value for comparison
- * @returns The created {@link QueryFieldFilterConstraint}.
- */
- function where(fieldPath, opStr, value) {
- const op = opStr;
- const field = fieldPathFromArgument('where', fieldPath);
- return QueryFieldFilterConstraint._create(field, op, value);
- }
- /**
- * A `QueryCompositeFilterConstraint` is used to narrow the set of documents
- * returned by a Firestore query by performing the logical OR or AND of multiple
- * {@link QueryFieldFilterConstraint}s or {@link QueryCompositeFilterConstraint}s.
- * `QueryCompositeFilterConstraint`s are created by invoking {@link or} or
- * {@link and} and can then be passed to {@link (query:1)} to create a new query
- * instance that also contains the `QueryCompositeFilterConstraint`.
- */
- class QueryCompositeFilterConstraint extends AppliableConstraint {
- /**
- * @internal
- */
- constructor(
- /** The type of this query constraint */
- type, _queryConstraints) {
- super();
- this.type = type;
- this._queryConstraints = _queryConstraints;
- }
- static _create(type, _queryConstraints) {
- return new QueryCompositeFilterConstraint(type, _queryConstraints);
- }
- _parse(query) {
- const parsedFilters = this._queryConstraints
- .map(queryConstraint => {
- return queryConstraint._parse(query);
- })
- .filter(parsedFilter => parsedFilter.getFilters().length > 0);
- if (parsedFilters.length === 1) {
- return parsedFilters[0];
- }
- return CompositeFilter.create(parsedFilters, this._getOperator());
- }
- _apply(query) {
- const parsedFilter = this._parse(query);
- if (parsedFilter.getFilters().length === 0) {
- // Return the existing query if not adding any more filters (e.g. an empty
- // composite filter).
- return query;
- }
- validateNewFilter(query._query, parsedFilter);
- return new Query(query.firestore, query.converter, queryWithAddedFilter(query._query, parsedFilter));
- }
- _getQueryConstraints() {
- return this._queryConstraints;
- }
- _getOperator() {
- return this.type === 'and' ? "and" /* CompositeOperator.AND */ : "or" /* CompositeOperator.OR */;
- }
- }
- /**
- * Creates a new {@link QueryCompositeFilterConstraint} that is a disjunction of
- * the given filter constraints. A disjunction filter includes a document if it
- * satisfies any of the given filters.
- *
- * @param queryConstraints - Optional. The list of
- * {@link QueryFilterConstraint}s to perform a disjunction for. These must be
- * created with calls to {@link where}, {@link or}, or {@link and}.
- * @returns The newly created {@link QueryCompositeFilterConstraint}.
- */
- function or(...queryConstraints) {
- // Only support QueryFilterConstraints
- queryConstraints.forEach(queryConstraint => validateQueryFilterConstraint('or', queryConstraint));
- return QueryCompositeFilterConstraint._create("or" /* CompositeOperator.OR */, queryConstraints);
- }
- /**
- * Creates a new {@link QueryCompositeFilterConstraint} that is a conjunction of
- * the given filter constraints. A conjunction filter includes a document if it
- * satisfies all of the given filters.
- *
- * @param queryConstraints - Optional. The list of
- * {@link QueryFilterConstraint}s to perform a conjunction for. These must be
- * created with calls to {@link where}, {@link or}, or {@link and}.
- * @returns The newly created {@link QueryCompositeFilterConstraint}.
- */
- function and(...queryConstraints) {
- // Only support QueryFilterConstraints
- queryConstraints.forEach(queryConstraint => validateQueryFilterConstraint('and', queryConstraint));
- return QueryCompositeFilterConstraint._create("and" /* CompositeOperator.AND */, queryConstraints);
- }
- /**
- * A `QueryOrderByConstraint` is used to sort the set of documents returned by a
- * Firestore query. `QueryOrderByConstraint`s are created by invoking
- * {@link orderBy} and can then be passed to {@link (query:1)} to create a new query
- * instance that also contains this `QueryOrderByConstraint`.
- *
- * Note: Documents that do not contain the orderBy field will not be present in
- * the query result.
- */
- class QueryOrderByConstraint extends QueryConstraint {
- /**
- * @internal
- */
- constructor(_field, _direction) {
- super();
- this._field = _field;
- this._direction = _direction;
- /** The type of this query constraint */
- this.type = 'orderBy';
- }
- static _create(_field, _direction) {
- return new QueryOrderByConstraint(_field, _direction);
- }
- _apply(query) {
- const orderBy = newQueryOrderBy(query._query, this._field, this._direction);
- return new Query(query.firestore, query.converter, queryWithAddedOrderBy(query._query, orderBy));
- }
- }
- /**
- * Creates a {@link QueryOrderByConstraint} that sorts the query result by the
- * specified field, optionally in descending order instead of ascending.
- *
- * Note: Documents that do not contain the specified field will not be present
- * in the query result.
- *
- * @param fieldPath - The field to sort by.
- * @param directionStr - Optional direction to sort by ('asc' or 'desc'). If
- * not specified, order will be ascending.
- * @returns The created {@link QueryOrderByConstraint}.
- */
- function orderBy(fieldPath, directionStr = 'asc') {
- const direction = directionStr;
- const path = fieldPathFromArgument('orderBy', fieldPath);
- return QueryOrderByConstraint._create(path, direction);
- }
- /**
- * A `QueryLimitConstraint` is used to limit the number of documents returned by
- * a Firestore query.
- * `QueryLimitConstraint`s are created by invoking {@link limit} or
- * {@link limitToLast} and can then be passed to {@link (query:1)} to create a new
- * query instance that also contains this `QueryLimitConstraint`.
- */
- class QueryLimitConstraint extends QueryConstraint {
- /**
- * @internal
- */
- constructor(
- /** The type of this query constraint */
- type, _limit, _limitType) {
- super();
- this.type = type;
- this._limit = _limit;
- this._limitType = _limitType;
- }
- static _create(type, _limit, _limitType) {
- return new QueryLimitConstraint(type, _limit, _limitType);
- }
- _apply(query) {
- return new Query(query.firestore, query.converter, queryWithLimit(query._query, this._limit, this._limitType));
- }
- }
- /**
- * Creates a {@link QueryLimitConstraint} that only returns the first matching
- * documents.
- *
- * @param limit - The maximum number of items to return.
- * @returns The created {@link QueryLimitConstraint}.
- */
- function limit(limit) {
- validatePositiveNumber('limit', limit);
- return QueryLimitConstraint._create('limit', limit, "F" /* LimitType.First */);
- }
- /**
- * Creates a {@link QueryLimitConstraint} that only returns the last matching
- * documents.
- *
- * You must specify at least one `orderBy` clause for `limitToLast` queries,
- * otherwise an exception will be thrown during execution.
- *
- * @param limit - The maximum number of items to return.
- * @returns The created {@link QueryLimitConstraint}.
- */
- function limitToLast(limit) {
- validatePositiveNumber('limitToLast', limit);
- return QueryLimitConstraint._create('limitToLast', limit, "L" /* LimitType.Last */);
- }
- /**
- * A `QueryStartAtConstraint` is used to exclude documents from the start of a
- * result set returned by a Firestore query.
- * `QueryStartAtConstraint`s are created by invoking {@link (startAt:1)} or
- * {@link (startAfter:1)} and can then be passed to {@link (query:1)} to create a
- * new query instance that also contains this `QueryStartAtConstraint`.
- */
- class QueryStartAtConstraint extends QueryConstraint {
- /**
- * @internal
- */
- constructor(
- /** The type of this query constraint */
- type, _docOrFields, _inclusive) {
- super();
- this.type = type;
- this._docOrFields = _docOrFields;
- this._inclusive = _inclusive;
- }
- static _create(type, _docOrFields, _inclusive) {
- return new QueryStartAtConstraint(type, _docOrFields, _inclusive);
- }
- _apply(query) {
- const bound = newQueryBoundFromDocOrFields(query, this.type, this._docOrFields, this._inclusive);
- return new Query(query.firestore, query.converter, queryWithStartAt(query._query, bound));
- }
- }
- function startAt(...docOrFields) {
- return QueryStartAtConstraint._create('startAt', docOrFields,
- /*inclusive=*/ true);
- }
- function startAfter(...docOrFields) {
- return QueryStartAtConstraint._create('startAfter', docOrFields,
- /*inclusive=*/ false);
- }
- /**
- * A `QueryEndAtConstraint` is used to exclude documents from the end of a
- * result set returned by a Firestore query.
- * `QueryEndAtConstraint`s are created by invoking {@link (endAt:1)} or
- * {@link (endBefore:1)} and can then be passed to {@link (query:1)} to create a new
- * query instance that also contains this `QueryEndAtConstraint`.
- */
- class QueryEndAtConstraint extends QueryConstraint {
- /**
- * @internal
- */
- constructor(
- /** The type of this query constraint */
- type, _docOrFields, _inclusive) {
- super();
- this.type = type;
- this._docOrFields = _docOrFields;
- this._inclusive = _inclusive;
- }
- static _create(type, _docOrFields, _inclusive) {
- return new QueryEndAtConstraint(type, _docOrFields, _inclusive);
- }
- _apply(query) {
- const bound = newQueryBoundFromDocOrFields(query, this.type, this._docOrFields, this._inclusive);
- return new Query(query.firestore, query.converter, queryWithEndAt(query._query, bound));
- }
- }
- function endBefore(...docOrFields) {
- return QueryEndAtConstraint._create('endBefore', docOrFields,
- /*inclusive=*/ false);
- }
- function endAt(...docOrFields) {
- return QueryEndAtConstraint._create('endAt', docOrFields,
- /*inclusive=*/ true);
- }
- /** Helper function to create a bound from a document or fields */
- function newQueryBoundFromDocOrFields(query, methodName, docOrFields, inclusive) {
- docOrFields[0] = util.getModularInstance(docOrFields[0]);
- if (docOrFields[0] instanceof DocumentSnapshot$1) {
- return newQueryBoundFromDocument(query._query, query.firestore._databaseId, methodName, docOrFields[0]._document, inclusive);
- }
- else {
- const reader = newUserDataReader(query.firestore);
- return newQueryBoundFromFields(query._query, query.firestore._databaseId, reader, methodName, docOrFields, inclusive);
- }
- }
- function newQueryFilter(query, methodName, dataReader, databaseId, fieldPath, op, value) {
- let fieldValue;
- if (fieldPath.isKeyField()) {
- if (op === "array-contains" /* Operator.ARRAY_CONTAINS */ || op === "array-contains-any" /* Operator.ARRAY_CONTAINS_ANY */) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid Query. You can't perform '${op}' queries on documentId().`);
- }
- else if (op === "in" /* Operator.IN */ || op === "not-in" /* Operator.NOT_IN */) {
- validateDisjunctiveFilterElements(value, op);
- const referenceList = [];
- for (const arrayValue of value) {
- referenceList.push(parseDocumentIdValue(databaseId, query, arrayValue));
- }
- fieldValue = { arrayValue: { values: referenceList } };
- }
- else {
- fieldValue = parseDocumentIdValue(databaseId, query, value);
- }
- }
- else {
- if (op === "in" /* Operator.IN */ ||
- op === "not-in" /* Operator.NOT_IN */ ||
- op === "array-contains-any" /* Operator.ARRAY_CONTAINS_ANY */) {
- validateDisjunctiveFilterElements(value, op);
- }
- fieldValue = parseQueryValue(dataReader, methodName, value,
- /* allowArrays= */ op === "in" /* Operator.IN */ || op === "not-in" /* Operator.NOT_IN */);
- }
- const filter = FieldFilter.create(fieldPath, op, fieldValue);
- return filter;
- }
- function newQueryOrderBy(query, fieldPath, direction) {
- if (query.startAt !== null) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Invalid query. You must not call startAt() or startAfter() before ' +
- 'calling orderBy().');
- }
- if (query.endAt !== null) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Invalid query. You must not call endAt() or endBefore() before ' +
- 'calling orderBy().');
- }
- const orderBy = new OrderBy(fieldPath, direction);
- validateNewOrderBy(query, orderBy);
- return orderBy;
- }
- /**
- * Create a `Bound` from a query and a document.
- *
- * Note that the `Bound` will always include the key of the document
- * and so only the provided document will compare equal to the returned
- * position.
- *
- * Will throw if the document does not contain all fields of the order by
- * of the query or if any of the fields in the order by are an uncommitted
- * server timestamp.
- */
- function newQueryBoundFromDocument(query, databaseId, methodName, doc, inclusive) {
- if (!doc) {
- throw new FirestoreError(Code.NOT_FOUND, `Can't use a DocumentSnapshot that doesn't exist for ` +
- `${methodName}().`);
- }
- const components = [];
- // Because people expect to continue/end a query at the exact document
- // provided, we need to use the implicit sort order rather than the explicit
- // sort order, because it's guaranteed to contain the document key. That way
- // the position becomes unambiguous and the query continues/ends exactly at
- // the provided document. Without the key (by using the explicit sort
- // orders), multiple documents could match the position, yielding duplicate
- // results.
- for (const orderBy of queryOrderBy(query)) {
- if (orderBy.field.isKeyField()) {
- components.push(refValue(databaseId, doc.key));
- }
- else {
- const value = doc.data.field(orderBy.field);
- if (isServerTimestamp(value)) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Invalid query. You are trying to start or end a query using a ' +
- 'document for which the field "' +
- orderBy.field +
- '" is an uncommitted server timestamp. (Since the value of ' +
- 'this field is unknown, you cannot start/end a query with it.)');
- }
- else if (value !== null) {
- components.push(value);
- }
- else {
- const field = orderBy.field.canonicalString();
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid query. You are trying to start or end a query using a ` +
- `document for which the field '${field}' (used as the ` +
- `orderBy) does not exist.`);
- }
- }
- }
- return new Bound(components, inclusive);
- }
- /**
- * Converts a list of field values to a `Bound` for the given query.
- */
- function newQueryBoundFromFields(query, databaseId, dataReader, methodName, values, inclusive) {
- // Use explicit order by's because it has to match the query the user made
- const orderBy = query.explicitOrderBy;
- if (values.length > orderBy.length) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Too many arguments provided to ${methodName}(). ` +
- `The number of arguments must be less than or equal to the ` +
- `number of orderBy() clauses`);
- }
- const components = [];
- for (let i = 0; i < values.length; i++) {
- const rawValue = values[i];
- const orderByComponent = orderBy[i];
- if (orderByComponent.field.isKeyField()) {
- if (typeof rawValue !== 'string') {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid query. Expected a string for document ID in ` +
- `${methodName}(), but got a ${typeof rawValue}`);
- }
- if (!isCollectionGroupQuery(query) && rawValue.indexOf('/') !== -1) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid query. When querying a collection and ordering by documentId(), ` +
- `the value passed to ${methodName}() must be a plain document ID, but ` +
- `'${rawValue}' contains a slash.`);
- }
- const path = query.path.child(ResourcePath.fromString(rawValue));
- if (!DocumentKey.isDocumentKey(path)) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid query. When querying a collection group and ordering by ` +
- `documentId(), the value passed to ${methodName}() must result in a ` +
- `valid document path, but '${path}' is not because it contains an odd number ` +
- `of segments.`);
- }
- const key = new DocumentKey(path);
- components.push(refValue(databaseId, key));
- }
- else {
- const wrapped = parseQueryValue(dataReader, methodName, rawValue);
- components.push(wrapped);
- }
- }
- return new Bound(components, inclusive);
- }
- /**
- * Parses the given `documentIdValue` into a `ReferenceValue`, throwing
- * appropriate errors if the value is anything other than a `DocumentReference`
- * or `string`, or if the string is malformed.
- */
- function parseDocumentIdValue(databaseId, query, documentIdValue) {
- documentIdValue = util.getModularInstance(documentIdValue);
- if (typeof documentIdValue === 'string') {
- if (documentIdValue === '') {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Invalid query. When querying with documentId(), you ' +
- 'must provide a valid document ID, but it was an empty string.');
- }
- if (!isCollectionGroupQuery(query) && documentIdValue.indexOf('/') !== -1) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid query. When querying a collection by ` +
- `documentId(), you must provide a plain document ID, but ` +
- `'${documentIdValue}' contains a '/' character.`);
- }
- const path = query.path.child(ResourcePath.fromString(documentIdValue));
- if (!DocumentKey.isDocumentKey(path)) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid query. When querying a collection group by ` +
- `documentId(), the value provided must result in a valid document path, ` +
- `but '${path}' is not because it has an odd number of segments (${path.length}).`);
- }
- return refValue(databaseId, new DocumentKey(path));
- }
- else if (documentIdValue instanceof DocumentReference) {
- return refValue(databaseId, documentIdValue._key);
- }
- else {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid query. When querying with documentId(), you must provide a valid ` +
- `string or a DocumentReference, but it was: ` +
- `${valueDescription(documentIdValue)}.`);
- }
- }
- /**
- * Validates that the value passed into a disjunctive filter satisfies all
- * array requirements.
- */
- function validateDisjunctiveFilterElements(value, operator) {
- if (!Array.isArray(value) || value.length === 0) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Invalid Query. A non-empty array is required for ' +
- `'${operator.toString()}' filters.`);
- }
- }
- /**
- * Given an operator, returns the set of operators that cannot be used with it.
- *
- * This is not a comprehensive check, and this function should be removed in the
- * long term. Validations should occur in the Firestore backend.
- *
- * Operators in a query must adhere to the following set of rules:
- * 1. Only one inequality per query.
- * 2. `NOT_IN` cannot be used with array, disjunctive, or `NOT_EQUAL` operators.
- */
- function conflictingOps(op) {
- switch (op) {
- case "!=" /* Operator.NOT_EQUAL */:
- return ["!=" /* Operator.NOT_EQUAL */, "not-in" /* Operator.NOT_IN */];
- case "array-contains-any" /* Operator.ARRAY_CONTAINS_ANY */:
- case "in" /* Operator.IN */:
- return ["not-in" /* Operator.NOT_IN */];
- case "not-in" /* Operator.NOT_IN */:
- return [
- "array-contains-any" /* Operator.ARRAY_CONTAINS_ANY */,
- "in" /* Operator.IN */,
- "not-in" /* Operator.NOT_IN */,
- "!=" /* Operator.NOT_EQUAL */
- ];
- default:
- return [];
- }
- }
- function validateNewFieldFilter(query, fieldFilter) {
- if (fieldFilter.isInequality()) {
- const existingInequality = getInequalityFilterField(query);
- const newInequality = fieldFilter.field;
- if (existingInequality !== null &&
- !existingInequality.isEqual(newInequality)) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Invalid query. All where filters with an inequality' +
- ' (<, <=, !=, not-in, >, or >=) must be on the same field. But you have' +
- ` inequality filters on '${existingInequality.toString()}'` +
- ` and '${newInequality.toString()}'`);
- }
- const firstOrderByField = getFirstOrderByField(query);
- if (firstOrderByField !== null) {
- validateOrderByAndInequalityMatch(query, newInequality, firstOrderByField);
- }
- }
- const conflictingOp = findOpInsideFilters(query.filters, conflictingOps(fieldFilter.op));
- if (conflictingOp !== null) {
- // Special case when it's a duplicate op to give a slightly clearer error message.
- if (conflictingOp === fieldFilter.op) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Invalid query. You cannot use more than one ' +
- `'${fieldFilter.op.toString()}' filter.`);
- }
- else {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid query. You cannot use '${fieldFilter.op.toString()}' filters ` +
- `with '${conflictingOp.toString()}' filters.`);
- }
- }
- }
- function validateNewFilter(query, filter) {
- let testQuery = query;
- const subFilters = filter.getFlattenedFilters();
- for (const subFilter of subFilters) {
- validateNewFieldFilter(testQuery, subFilter);
- testQuery = queryWithAddedFilter(testQuery, subFilter);
- }
- }
- // Checks if any of the provided filter operators are included in the given list of filters and
- // returns the first one that is, or null if none are.
- function findOpInsideFilters(filters, operators) {
- for (const filter of filters) {
- for (const fieldFilter of filter.getFlattenedFilters()) {
- if (operators.indexOf(fieldFilter.op) >= 0) {
- return fieldFilter.op;
- }
- }
- }
- return null;
- }
- function validateNewOrderBy(query, orderBy) {
- if (getFirstOrderByField(query) === null) {
- // This is the first order by. It must match any inequality.
- const inequalityField = getInequalityFilterField(query);
- if (inequalityField !== null) {
- validateOrderByAndInequalityMatch(query, inequalityField, orderBy.field);
- }
- }
- }
- function validateOrderByAndInequalityMatch(baseQuery, inequality, orderBy) {
- if (!orderBy.isEqual(inequality)) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid query. You have a where filter with an inequality ` +
- `(<, <=, !=, not-in, >, or >=) on field '${inequality.toString()}' ` +
- `and so you must also use '${inequality.toString()}' ` +
- `as your first argument to orderBy(), but your first orderBy() ` +
- `is on field '${orderBy.toString()}' instead.`);
- }
- }
- function validateQueryFilterConstraint(functionName, queryConstraint) {
- if (!(queryConstraint instanceof QueryFieldFilterConstraint) &&
- !(queryConstraint instanceof QueryCompositeFilterConstraint)) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, `Function ${functionName}() requires AppliableConstraints created with a call to 'where(...)', 'or(...)', or 'and(...)'.`);
- }
- }
- function validateQueryConstraintArray(queryConstraint) {
- const compositeFilterCount = queryConstraint.filter(filter => filter instanceof QueryCompositeFilterConstraint).length;
- const fieldFilterCount = queryConstraint.filter(filter => filter instanceof QueryFieldFilterConstraint).length;
- if (compositeFilterCount > 1 ||
- (compositeFilterCount > 0 && fieldFilterCount > 0)) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'InvalidQuery. When using composite filters, you cannot use ' +
- 'more than one filter at the top level. Consider nesting the multiple ' +
- 'filters within an `and(...)` statement. For example: ' +
- 'change `query(query, where(...), or(...))` to ' +
- '`query(query, and(where(...), or(...)))`.');
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Converts Firestore's internal types to the JavaScript types that we expose
- * to the user.
- *
- * @internal
- */
- class AbstractUserDataWriter {
- convertValue(value, serverTimestampBehavior = 'none') {
- switch (typeOrder(value)) {
- case 0 /* TypeOrder.NullValue */:
- return null;
- case 1 /* TypeOrder.BooleanValue */:
- return value.booleanValue;
- case 2 /* TypeOrder.NumberValue */:
- return normalizeNumber(value.integerValue || value.doubleValue);
- case 3 /* TypeOrder.TimestampValue */:
- return this.convertTimestamp(value.timestampValue);
- case 4 /* TypeOrder.ServerTimestampValue */:
- return this.convertServerTimestamp(value, serverTimestampBehavior);
- case 5 /* TypeOrder.StringValue */:
- return value.stringValue;
- case 6 /* TypeOrder.BlobValue */:
- return this.convertBytes(normalizeByteString(value.bytesValue));
- case 7 /* TypeOrder.RefValue */:
- return this.convertReference(value.referenceValue);
- case 8 /* TypeOrder.GeoPointValue */:
- return this.convertGeoPoint(value.geoPointValue);
- case 9 /* TypeOrder.ArrayValue */:
- return this.convertArray(value.arrayValue, serverTimestampBehavior);
- case 10 /* TypeOrder.ObjectValue */:
- return this.convertObject(value.mapValue, serverTimestampBehavior);
- default:
- throw fail();
- }
- }
- convertObject(mapValue, serverTimestampBehavior) {
- return this.convertObjectMap(mapValue.fields, serverTimestampBehavior);
- }
- /**
- * @internal
- */
- convertObjectMap(fields, serverTimestampBehavior = 'none') {
- const result = {};
- forEach(fields, (key, value) => {
- result[key] = this.convertValue(value, serverTimestampBehavior);
- });
- return result;
- }
- convertGeoPoint(value) {
- return new GeoPoint(normalizeNumber(value.latitude), normalizeNumber(value.longitude));
- }
- convertArray(arrayValue, serverTimestampBehavior) {
- return (arrayValue.values || []).map(value => this.convertValue(value, serverTimestampBehavior));
- }
- convertServerTimestamp(value, serverTimestampBehavior) {
- switch (serverTimestampBehavior) {
- case 'previous':
- const previousValue = getPreviousValue(value);
- if (previousValue == null) {
- return null;
- }
- return this.convertValue(previousValue, serverTimestampBehavior);
- case 'estimate':
- return this.convertTimestamp(getLocalWriteTime(value));
- default:
- return null;
- }
- }
- convertTimestamp(value) {
- const normalizedValue = normalizeTimestamp(value);
- return new Timestamp(normalizedValue.seconds, normalizedValue.nanos);
- }
- convertDocumentKey(name, expectedDatabaseId) {
- const resourcePath = ResourcePath.fromString(name);
- hardAssert(isValidResourceName(resourcePath));
- const databaseId = new DatabaseId(resourcePath.get(1), resourcePath.get(3));
- const key = new DocumentKey(resourcePath.popFirst(5));
- if (!databaseId.isEqual(expectedDatabaseId)) {
- // TODO(b/64130202): Somehow support foreign references.
- logError(`Document ${key} contains a document ` +
- `reference within a different database (` +
- `${databaseId.projectId}/${databaseId.database}) which is not ` +
- `supported. It will be treated as a reference in the current ` +
- `database (${expectedDatabaseId.projectId}/${expectedDatabaseId.database}) ` +
- `instead.`);
- }
- return key;
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Converts custom model object of type T into `DocumentData` by applying the
- * converter if it exists.
- *
- * This function is used when converting user objects to `DocumentData`
- * because we want to provide the user with a more specific error message if
- * their `set()` or fails due to invalid data originating from a `toFirestore()`
- * call.
- */
- function applyFirestoreDataConverter(converter, value, options) {
- let convertedValue;
- if (converter) {
- if (options && (options.merge || options.mergeFields)) {
- // Cast to `any` in order to satisfy the union type constraint on
- // toFirestore().
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- convertedValue = converter.toFirestore(value, options);
- }
- else {
- convertedValue = converter.toFirestore(value);
- }
- }
- else {
- convertedValue = value;
- }
- return convertedValue;
- }
- class LiteUserDataWriter extends AbstractUserDataWriter {
- constructor(firestore) {
- super();
- this.firestore = firestore;
- }
- convertBytes(bytes) {
- return new Bytes(bytes);
- }
- convertReference(name) {
- const key = this.convertDocumentKey(name, this.firestore._databaseId);
- return new DocumentReference(this.firestore, /* converter= */ null, key);
- }
- }
- /**
- * @license
- * Copyright 2022 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Create an AggregateField object that can be used to compute the sum of
- * a specified field over a range of documents in the result set of a query.
- * @param field Specifies the field to sum across the result set.
- * @internal TODO (sum/avg) remove when public
- */
- function sum(field) {
- return new AggregateField('sum', fieldPathFromArgument$1('sum', field));
- }
- /**
- * Create an AggregateField object that can be used to compute the average of
- * a specified field over a range of documents in the result set of a query.
- * @param field Specifies the field to average across the result set.
- * @internal TODO (sum/avg) remove when public
- */
- function average(field) {
- return new AggregateField('avg', fieldPathFromArgument$1('average', field));
- }
- /**
- * Create an AggregateField object that can be used to compute the count of
- * documents in the result set of a query.
- * @internal TODO (sum/avg) remove when public
- */
- function count() {
- return new AggregateField('count');
- }
- /**
- * Compares two 'AggregateField` instances for equality.
- *
- * @param left Compare this AggregateField to the `right`.
- * @param right Compare this AggregateField to the `left`.
- * @internal TODO (sum/avg) remove when public
- */
- function aggregateFieldEqual(left, right) {
- var _a, _b;
- return (left instanceof AggregateField &&
- right instanceof AggregateField &&
- left._aggregateType === right._aggregateType &&
- ((_a = left._internalFieldPath) === null || _a === void 0 ? void 0 : _a.canonicalString()) ===
- ((_b = right._internalFieldPath) === null || _b === void 0 ? void 0 : _b.canonicalString()));
- }
- /**
- * Compares two `AggregateQuerySnapshot` instances for equality.
- *
- * Two `AggregateQuerySnapshot` instances are considered "equal" if they have
- * underlying queries that compare equal, and the same data.
- *
- * @param left - The first `AggregateQuerySnapshot` to compare.
- * @param right - The second `AggregateQuerySnapshot` to compare.
- *
- * @returns `true` if the objects are "equal", as defined above, or `false`
- * otherwise.
- */
- function aggregateQuerySnapshotEqual(left, right) {
- return (queryEqual(left.query, right.query) && util.deepEqual(left.data(), right.data()));
- }
- /**
- * @license
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- function isPartialObserver(obj) {
- return implementsAnyMethods(obj, ['next', 'error', 'complete']);
- }
- /**
- * Returns true if obj is an object and contains at least one of the specified
- * methods.
- */
- function implementsAnyMethods(obj, methods) {
- if (typeof obj !== 'object' || obj === null) {
- return false;
- }
- const object = obj;
- for (const method of methods) {
- if (method in object && typeof object[method] === 'function') {
- return true;
- }
- }
- return false;
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Metadata about a snapshot, describing the state of the snapshot.
- */
- class SnapshotMetadata {
- /** @hideconstructor */
- constructor(hasPendingWrites, fromCache) {
- this.hasPendingWrites = hasPendingWrites;
- this.fromCache = fromCache;
- }
- /**
- * Returns true if this `SnapshotMetadata` is equal to the provided one.
- *
- * @param other - The `SnapshotMetadata` to compare against.
- * @returns true if this `SnapshotMetadata` is equal to the provided one.
- */
- isEqual(other) {
- return (this.hasPendingWrites === other.hasPendingWrites &&
- this.fromCache === other.fromCache);
- }
- }
- /**
- * A `DocumentSnapshot` contains data read from a document in your Firestore
- * database. The data can be extracted with `.data()` or `.get(<field>)` to
- * get a specific field.
- *
- * For a `DocumentSnapshot` that points to a non-existing document, any data
- * access will return 'undefined'. You can use the `exists()` method to
- * explicitly verify a document's existence.
- */
- class DocumentSnapshot extends DocumentSnapshot$1 {
- /** @hideconstructor protected */
- constructor(_firestore, userDataWriter, key, document, metadata, converter) {
- super(_firestore, userDataWriter, key, document, converter);
- this._firestore = _firestore;
- this._firestoreImpl = _firestore;
- this.metadata = metadata;
- }
- /**
- * Returns whether or not the data exists. True if the document exists.
- */
- exists() {
- return super.exists();
- }
- /**
- * Retrieves all fields in the document as an `Object`. Returns `undefined` if
- * the document doesn't exist.
- *
- * By default, `serverTimestamp()` values that have not yet been
- * set to their final value will be returned as `null`. You can override
- * this by passing an options object.
- *
- * @param options - An options object to configure how data is retrieved from
- * the snapshot (for example the desired behavior for server timestamps that
- * have not yet been set to their final value).
- * @returns An `Object` containing all fields in the document or `undefined` if
- * the document doesn't exist.
- */
- data(options = {}) {
- if (!this._document) {
- return undefined;
- }
- else if (this._converter) {
- // We only want to use the converter and create a new DocumentSnapshot
- // if a converter has been provided.
- const snapshot = new QueryDocumentSnapshot(this._firestore, this._userDataWriter, this._key, this._document, this.metadata,
- /* converter= */ null);
- return this._converter.fromFirestore(snapshot, options);
- }
- else {
- return this._userDataWriter.convertValue(this._document.data.value, options.serverTimestamps);
- }
- }
- /**
- * Retrieves the field specified by `fieldPath`. Returns `undefined` if the
- * document or field doesn't exist.
- *
- * By default, a `serverTimestamp()` that has not yet been set to
- * its final value will be returned as `null`. You can override this by
- * passing an options object.
- *
- * @param fieldPath - The path (for example 'foo' or 'foo.bar') to a specific
- * field.
- * @param options - An options object to configure how the field is retrieved
- * from the snapshot (for example the desired behavior for server timestamps
- * that have not yet been set to their final value).
- * @returns The data at the specified field location or undefined if no such
- * field exists in the document.
- */
- // We are using `any` here to avoid an explicit cast by our users.
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- get(fieldPath, options = {}) {
- if (this._document) {
- const value = this._document.data.field(fieldPathFromArgument('DocumentSnapshot.get', fieldPath));
- if (value !== null) {
- return this._userDataWriter.convertValue(value, options.serverTimestamps);
- }
- }
- return undefined;
- }
- }
- /**
- * A `QueryDocumentSnapshot` contains data read from a document in your
- * Firestore database as part of a query. The document is guaranteed to exist
- * and its data can be extracted with `.data()` or `.get(<field>)` to get a
- * specific field.
- *
- * A `QueryDocumentSnapshot` offers the same API surface as a
- * `DocumentSnapshot`. Since query results contain only existing documents, the
- * `exists` property will always be true and `data()` will never return
- * 'undefined'.
- */
- class QueryDocumentSnapshot extends DocumentSnapshot {
- /**
- * Retrieves all fields in the document as an `Object`.
- *
- * By default, `serverTimestamp()` values that have not yet been
- * set to their final value will be returned as `null`. You can override
- * this by passing an options object.
- *
- * @override
- * @param options - An options object to configure how data is retrieved from
- * the snapshot (for example the desired behavior for server timestamps that
- * have not yet been set to their final value).
- * @returns An `Object` containing all fields in the document.
- */
- data(options = {}) {
- return super.data(options);
- }
- }
- /**
- * A `QuerySnapshot` contains zero or more `DocumentSnapshot` objects
- * representing the results of a query. The documents can be accessed as an
- * array via the `docs` property or enumerated using the `forEach` method. The
- * number of documents can be determined via the `empty` and `size`
- * properties.
- */
- class QuerySnapshot {
- /** @hideconstructor */
- constructor(_firestore, _userDataWriter, query, _snapshot) {
- this._firestore = _firestore;
- this._userDataWriter = _userDataWriter;
- this._snapshot = _snapshot;
- this.metadata = new SnapshotMetadata(_snapshot.hasPendingWrites, _snapshot.fromCache);
- this.query = query;
- }
- /** An array of all the documents in the `QuerySnapshot`. */
- get docs() {
- const result = [];
- this.forEach(doc => result.push(doc));
- return result;
- }
- /** The number of documents in the `QuerySnapshot`. */
- get size() {
- return this._snapshot.docs.size;
- }
- /** True if there are no documents in the `QuerySnapshot`. */
- get empty() {
- return this.size === 0;
- }
- /**
- * Enumerates all of the documents in the `QuerySnapshot`.
- *
- * @param callback - A callback to be called with a `QueryDocumentSnapshot` for
- * each document in the snapshot.
- * @param thisArg - The `this` binding for the callback.
- */
- forEach(callback, thisArg) {
- this._snapshot.docs.forEach(doc => {
- callback.call(thisArg, new QueryDocumentSnapshot(this._firestore, this._userDataWriter, doc.key, doc, new SnapshotMetadata(this._snapshot.mutatedKeys.has(doc.key), this._snapshot.fromCache), this.query.converter));
- });
- }
- /**
- * Returns an array of the documents changes since the last snapshot. If this
- * is the first snapshot, all documents will be in the list as 'added'
- * changes.
- *
- * @param options - `SnapshotListenOptions` that control whether metadata-only
- * changes (i.e. only `DocumentSnapshot.metadata` changed) should trigger
- * snapshot events.
- */
- docChanges(options = {}) {
- const includeMetadataChanges = !!options.includeMetadataChanges;
- if (includeMetadataChanges && this._snapshot.excludesMetadataChanges) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'To include metadata changes with your document changes, you must ' +
- 'also pass { includeMetadataChanges:true } to onSnapshot().');
- }
- if (!this._cachedChanges ||
- this._cachedChangesIncludeMetadataChanges !== includeMetadataChanges) {
- this._cachedChanges = changesFromSnapshot(this, includeMetadataChanges);
- this._cachedChangesIncludeMetadataChanges = includeMetadataChanges;
- }
- return this._cachedChanges;
- }
- }
- /** Calculates the array of `DocumentChange`s for a given `ViewSnapshot`. */
- function changesFromSnapshot(querySnapshot, includeMetadataChanges) {
- if (querySnapshot._snapshot.oldDocs.isEmpty()) {
- let index = 0;
- return querySnapshot._snapshot.docChanges.map(change => {
- const doc = new QueryDocumentSnapshot(querySnapshot._firestore, querySnapshot._userDataWriter, change.doc.key, change.doc, new SnapshotMetadata(querySnapshot._snapshot.mutatedKeys.has(change.doc.key), querySnapshot._snapshot.fromCache), querySnapshot.query.converter);
- change.doc;
- return {
- type: 'added',
- doc,
- oldIndex: -1,
- newIndex: index++
- };
- });
- }
- else {
- // A `DocumentSet` that is updated incrementally as changes are applied to use
- // to lookup the index of a document.
- let indexTracker = querySnapshot._snapshot.oldDocs;
- return querySnapshot._snapshot.docChanges
- .filter(change => includeMetadataChanges || change.type !== 3 /* ChangeType.Metadata */)
- .map(change => {
- const doc = new QueryDocumentSnapshot(querySnapshot._firestore, querySnapshot._userDataWriter, change.doc.key, change.doc, new SnapshotMetadata(querySnapshot._snapshot.mutatedKeys.has(change.doc.key), querySnapshot._snapshot.fromCache), querySnapshot.query.converter);
- let oldIndex = -1;
- let newIndex = -1;
- if (change.type !== 0 /* ChangeType.Added */) {
- oldIndex = indexTracker.indexOf(change.doc.key);
- indexTracker = indexTracker.delete(change.doc.key);
- }
- if (change.type !== 1 /* ChangeType.Removed */) {
- indexTracker = indexTracker.add(change.doc);
- newIndex = indexTracker.indexOf(change.doc.key);
- }
- return {
- type: resultChangeType(change.type),
- doc,
- oldIndex,
- newIndex
- };
- });
- }
- }
- function resultChangeType(type) {
- switch (type) {
- case 0 /* ChangeType.Added */:
- return 'added';
- case 2 /* ChangeType.Modified */:
- case 3 /* ChangeType.Metadata */:
- return 'modified';
- case 1 /* ChangeType.Removed */:
- return 'removed';
- default:
- return fail();
- }
- }
- // TODO(firestoreexp): Add tests for snapshotEqual with different snapshot
- // metadata
- /**
- * Returns true if the provided snapshots are equal.
- *
- * @param left - A snapshot to compare.
- * @param right - A snapshot to compare.
- * @returns true if the snapshots are equal.
- */
- function snapshotEqual(left, right) {
- if (left instanceof DocumentSnapshot && right instanceof DocumentSnapshot) {
- return (left._firestore === right._firestore &&
- left._key.isEqual(right._key) &&
- (left._document === null
- ? right._document === null
- : left._document.isEqual(right._document)) &&
- left._converter === right._converter);
- }
- else if (left instanceof QuerySnapshot && right instanceof QuerySnapshot) {
- return (left._firestore === right._firestore &&
- queryEqual(left.query, right.query) &&
- left.metadata.isEqual(right.metadata) &&
- left._snapshot.isEqual(right._snapshot));
- }
- return false;
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Reads the document referred to by this `DocumentReference`.
- *
- * Note: `getDoc()` attempts to provide up-to-date data when possible by waiting
- * for data from the server, but it may return cached data or fail if you are
- * offline and the server cannot be reached. To specify this behavior, invoke
- * {@link getDocFromCache} or {@link getDocFromServer}.
- *
- * @param reference - The reference of the document to fetch.
- * @returns A Promise resolved with a `DocumentSnapshot` containing the
- * current document contents.
- */
- function getDoc(reference) {
- reference = cast(reference, DocumentReference);
- const firestore = cast(reference.firestore, Firestore);
- const client = ensureFirestoreConfigured(firestore);
- return firestoreClientGetDocumentViaSnapshotListener(client, reference._key).then(snapshot => convertToDocSnapshot(firestore, reference, snapshot));
- }
- class ExpUserDataWriter extends AbstractUserDataWriter {
- constructor(firestore) {
- super();
- this.firestore = firestore;
- }
- convertBytes(bytes) {
- return new Bytes(bytes);
- }
- convertReference(name) {
- const key = this.convertDocumentKey(name, this.firestore._databaseId);
- return new DocumentReference(this.firestore, /* converter= */ null, key);
- }
- }
- /**
- * Reads the document referred to by this `DocumentReference` from cache.
- * Returns an error if the document is not currently cached.
- *
- * @returns A `Promise` resolved with a `DocumentSnapshot` containing the
- * current document contents.
- */
- function getDocFromCache(reference) {
- reference = cast(reference, DocumentReference);
- const firestore = cast(reference.firestore, Firestore);
- const client = ensureFirestoreConfigured(firestore);
- const userDataWriter = new ExpUserDataWriter(firestore);
- return firestoreClientGetDocumentFromLocalCache(client, reference._key).then(doc => new DocumentSnapshot(firestore, userDataWriter, reference._key, doc, new SnapshotMetadata(doc !== null && doc.hasLocalMutations,
- /* fromCache= */ true), reference.converter));
- }
- /**
- * Reads the document referred to by this `DocumentReference` from the server.
- * Returns an error if the network is not available.
- *
- * @returns A `Promise` resolved with a `DocumentSnapshot` containing the
- * current document contents.
- */
- function getDocFromServer(reference) {
- reference = cast(reference, DocumentReference);
- const firestore = cast(reference.firestore, Firestore);
- const client = ensureFirestoreConfigured(firestore);
- return firestoreClientGetDocumentViaSnapshotListener(client, reference._key, {
- source: 'server'
- }).then(snapshot => convertToDocSnapshot(firestore, reference, snapshot));
- }
- /**
- * Executes the query and returns the results as a `QuerySnapshot`.
- *
- * Note: `getDocs()` attempts to provide up-to-date data when possible by
- * waiting for data from the server, but it may return cached data or fail if
- * you are offline and the server cannot be reached. To specify this behavior,
- * invoke {@link getDocsFromCache} or {@link getDocsFromServer}.
- *
- * @returns A `Promise` that will be resolved with the results of the query.
- */
- function getDocs(query) {
- query = cast(query, Query);
- const firestore = cast(query.firestore, Firestore);
- const client = ensureFirestoreConfigured(firestore);
- const userDataWriter = new ExpUserDataWriter(firestore);
- validateHasExplicitOrderByForLimitToLast(query._query);
- return firestoreClientGetDocumentsViaSnapshotListener(client, query._query).then(snapshot => new QuerySnapshot(firestore, userDataWriter, query, snapshot));
- }
- /**
- * Executes the query and returns the results as a `QuerySnapshot` from cache.
- * Returns an empty result set if no documents matching the query are currently
- * cached.
- *
- * @returns A `Promise` that will be resolved with the results of the query.
- */
- function getDocsFromCache(query) {
- query = cast(query, Query);
- const firestore = cast(query.firestore, Firestore);
- const client = ensureFirestoreConfigured(firestore);
- const userDataWriter = new ExpUserDataWriter(firestore);
- return firestoreClientGetDocumentsFromLocalCache(client, query._query).then(snapshot => new QuerySnapshot(firestore, userDataWriter, query, snapshot));
- }
- /**
- * Executes the query and returns the results as a `QuerySnapshot` from the
- * server. Returns an error if the network is not available.
- *
- * @returns A `Promise` that will be resolved with the results of the query.
- */
- function getDocsFromServer(query) {
- query = cast(query, Query);
- const firestore = cast(query.firestore, Firestore);
- const client = ensureFirestoreConfigured(firestore);
- const userDataWriter = new ExpUserDataWriter(firestore);
- return firestoreClientGetDocumentsViaSnapshotListener(client, query._query, {
- source: 'server'
- }).then(snapshot => new QuerySnapshot(firestore, userDataWriter, query, snapshot));
- }
- function setDoc(reference, data, options) {
- reference = cast(reference, DocumentReference);
- const firestore = cast(reference.firestore, Firestore);
- const convertedValue = applyFirestoreDataConverter(reference.converter, data, options);
- const dataReader = newUserDataReader(firestore);
- const parsed = parseSetData(dataReader, 'setDoc', reference._key, convertedValue, reference.converter !== null, options);
- const mutation = parsed.toMutation(reference._key, Precondition.none());
- return executeWrite(firestore, [mutation]);
- }
- function updateDoc(reference, fieldOrUpdateData, value, ...moreFieldsAndValues) {
- reference = cast(reference, DocumentReference);
- const firestore = cast(reference.firestore, Firestore);
- const dataReader = newUserDataReader(firestore);
- // For Compat types, we have to "extract" the underlying types before
- // performing validation.
- fieldOrUpdateData = util.getModularInstance(fieldOrUpdateData);
- let parsed;
- if (typeof fieldOrUpdateData === 'string' ||
- fieldOrUpdateData instanceof FieldPath) {
- parsed = parseUpdateVarargs(dataReader, 'updateDoc', reference._key, fieldOrUpdateData, value, moreFieldsAndValues);
- }
- else {
- parsed = parseUpdateData(dataReader, 'updateDoc', reference._key, fieldOrUpdateData);
- }
- const mutation = parsed.toMutation(reference._key, Precondition.exists(true));
- return executeWrite(firestore, [mutation]);
- }
- /**
- * Deletes the document referred to by the specified `DocumentReference`.
- *
- * @param reference - A reference to the document to delete.
- * @returns A Promise resolved once the document has been successfully
- * deleted from the backend (note that it won't resolve while you're offline).
- */
- function deleteDoc(reference) {
- const firestore = cast(reference.firestore, Firestore);
- const mutations = [new DeleteMutation(reference._key, Precondition.none())];
- return executeWrite(firestore, mutations);
- }
- /**
- * Add a new document to specified `CollectionReference` with the given data,
- * assigning it a document ID automatically.
- *
- * @param reference - A reference to the collection to add this document to.
- * @param data - An Object containing the data for the new document.
- * @returns A `Promise` resolved with a `DocumentReference` pointing to the
- * newly created document after it has been written to the backend (Note that it
- * won't resolve while you're offline).
- */
- function addDoc(reference, data) {
- const firestore = cast(reference.firestore, Firestore);
- const docRef = doc(reference);
- const convertedValue = applyFirestoreDataConverter(reference.converter, data);
- const dataReader = newUserDataReader(reference.firestore);
- const parsed = parseSetData(dataReader, 'addDoc', docRef._key, convertedValue, reference.converter !== null, {});
- const mutation = parsed.toMutation(docRef._key, Precondition.exists(false));
- return executeWrite(firestore, [mutation]).then(() => docRef);
- }
- function onSnapshot(reference, ...args) {
- var _a, _b, _c;
- reference = util.getModularInstance(reference);
- let options = {
- includeMetadataChanges: false
- };
- let currArg = 0;
- if (typeof args[currArg] === 'object' && !isPartialObserver(args[currArg])) {
- options = args[currArg];
- currArg++;
- }
- const internalOptions = {
- includeMetadataChanges: options.includeMetadataChanges
- };
- if (isPartialObserver(args[currArg])) {
- const userObserver = args[currArg];
- args[currArg] = (_a = userObserver.next) === null || _a === void 0 ? void 0 : _a.bind(userObserver);
- args[currArg + 1] = (_b = userObserver.error) === null || _b === void 0 ? void 0 : _b.bind(userObserver);
- args[currArg + 2] = (_c = userObserver.complete) === null || _c === void 0 ? void 0 : _c.bind(userObserver);
- }
- let observer;
- let firestore;
- let internalQuery;
- if (reference instanceof DocumentReference) {
- firestore = cast(reference.firestore, Firestore);
- internalQuery = newQueryForPath(reference._key.path);
- observer = {
- next: snapshot => {
- if (args[currArg]) {
- args[currArg](convertToDocSnapshot(firestore, reference, snapshot));
- }
- },
- error: args[currArg + 1],
- complete: args[currArg + 2]
- };
- }
- else {
- const query = cast(reference, Query);
- firestore = cast(query.firestore, Firestore);
- internalQuery = query._query;
- const userDataWriter = new ExpUserDataWriter(firestore);
- observer = {
- next: snapshot => {
- if (args[currArg]) {
- args[currArg](new QuerySnapshot(firestore, userDataWriter, query, snapshot));
- }
- },
- error: args[currArg + 1],
- complete: args[currArg + 2]
- };
- validateHasExplicitOrderByForLimitToLast(reference._query);
- }
- const client = ensureFirestoreConfigured(firestore);
- return firestoreClientListen(client, internalQuery, internalOptions, observer);
- }
- function onSnapshotsInSync(firestore, arg) {
- firestore = cast(firestore, Firestore);
- const client = ensureFirestoreConfigured(firestore);
- const observer = isPartialObserver(arg)
- ? arg
- : {
- next: arg
- };
- return firestoreClientAddSnapshotsInSyncListener(client, observer);
- }
- /**
- * Locally writes `mutations` on the async queue.
- * @internal
- */
- function executeWrite(firestore, mutations) {
- const client = ensureFirestoreConfigured(firestore);
- return firestoreClientWrite(client, mutations);
- }
- /**
- * Converts a {@link ViewSnapshot} that contains the single document specified by `ref`
- * to a {@link DocumentSnapshot}.
- */
- function convertToDocSnapshot(firestore, ref, snapshot) {
- const doc = snapshot.docs.get(ref._key);
- const userDataWriter = new ExpUserDataWriter(firestore);
- return new DocumentSnapshot(firestore, userDataWriter, ref._key, doc, new SnapshotMetadata(snapshot.hasPendingWrites, snapshot.fromCache), ref.converter);
- }
- /**
- * @license
- * Copyright 2022 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Calculates the number of documents in the result set of the given query,
- * without actually downloading the documents.
- *
- * Using this function to count the documents is efficient because only the
- * final count, not the documents' data, is downloaded. This function can even
- * count the documents if the result set would be prohibitively large to
- * download entirely (e.g. thousands of documents).
- *
- * The result received from the server is presented, unaltered, without
- * considering any local state. That is, documents in the local cache are not
- * taken into consideration, neither are local modifications not yet
- * synchronized with the server. Previously-downloaded results, if any, are not
- * used: every request using this source necessarily involves a round trip to
- * the server.
- *
- * @param query - The query whose result set size to calculate.
- * @returns A Promise that will be resolved with the count; the count can be
- * retrieved from `snapshot.data().count`, where `snapshot` is the
- * `AggregateQuerySnapshot` to which the returned Promise resolves.
- */
- function getCountFromServer(query) {
- const countQuerySpec = {
- count: count()
- };
- return getAggregateFromServer(query, countQuerySpec);
- }
- /**
- * Calculates the specified aggregations over the documents in the result
- * set of the given query, without actually downloading the documents.
- *
- * Using this function to perform aggregations is efficient because only the
- * final aggregation values, not the documents' data, is downloaded. This
- * function can even perform aggregations of the documents if the result set
- * would be prohibitively large to download entirely (e.g. thousands of documents).
- *
- * The result received from the server is presented, unaltered, without
- * considering any local state. That is, documents in the local cache are not
- * taken into consideration, neither are local modifications not yet
- * synchronized with the server. Previously-downloaded results, if any, are not
- * used: every request using this source necessarily involves a round trip to
- * the server.
- *
- * @param query The query whose result set to aggregate over.
- * @param aggregateSpec An `AggregateSpec` object that specifies the aggregates
- * to perform over the result set. The AggregateSpec specifies aliases for each
- * aggregate, which can be used to retrieve the aggregate result.
- * @example
- * ```typescript
- * const aggregateSnapshot = await getAggregateFromServer(query, {
- * countOfDocs: count(),
- * totalHours: sum('hours'),
- * averageScore: average('score')
- * });
- *
- * const countOfDocs: number = aggregateSnapshot.data().countOfDocs;
- * const totalHours: number = aggregateSnapshot.data().totalHours;
- * const averageScore: number | null = aggregateSnapshot.data().averageScore;
- * ```
- * @internal TODO (sum/avg) remove when public
- */
- function getAggregateFromServer(query, aggregateSpec) {
- const firestore = cast(query.firestore, Firestore);
- const client = ensureFirestoreConfigured(firestore);
- const internalAggregates = mapToArray(aggregateSpec, (aggregate, alias) => {
- return new AggregateImpl(alias, aggregate._aggregateType, aggregate._internalFieldPath);
- });
- // Run the aggregation and convert the results
- return firestoreClientRunAggregateQuery(client, query._query, internalAggregates).then(aggregateResult => convertToAggregateQuerySnapshot(firestore, query, aggregateResult));
- }
- /**
- * Converts the core aggregration result to an `AggregateQuerySnapshot`
- * that can be returned to the consumer.
- * @param query
- * @param aggregateResult Core aggregation result
- * @internal
- */
- function convertToAggregateQuerySnapshot(firestore, query, aggregateResult) {
- const userDataWriter = new ExpUserDataWriter(firestore);
- const querySnapshot = new AggregateQuerySnapshot(query, userDataWriter, aggregateResult);
- return querySnapshot;
- }
- /**
- * @license
- * Copyright 2023 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- class MemoryLocalCacheImpl {
- constructor(settings) {
- this.kind = 'memory';
- this._onlineComponentProvider = new OnlineComponentProvider();
- if (settings === null || settings === void 0 ? void 0 : settings.garbageCollector) {
- this._offlineComponentProvider =
- settings.garbageCollector._offlineComponentProvider;
- }
- else {
- this._offlineComponentProvider = new MemoryOfflineComponentProvider();
- }
- }
- toJSON() {
- return { kind: this.kind };
- }
- }
- class PersistentLocalCacheImpl {
- constructor(settings) {
- this.kind = 'persistent';
- let tabManager;
- if (settings === null || settings === void 0 ? void 0 : settings.tabManager) {
- settings.tabManager._initialize(settings);
- tabManager = settings.tabManager;
- }
- else {
- tabManager = persistentSingleTabManager(undefined);
- tabManager._initialize(settings);
- }
- this._onlineComponentProvider = tabManager._onlineComponentProvider;
- this._offlineComponentProvider = tabManager._offlineComponentProvider;
- }
- toJSON() {
- return { kind: this.kind };
- }
- }
- class MemoryEagerGabageCollectorImpl {
- constructor() {
- this.kind = 'memoryEager';
- this._offlineComponentProvider = new MemoryOfflineComponentProvider();
- }
- toJSON() {
- return { kind: this.kind };
- }
- }
- class MemoryLruGabageCollectorImpl {
- constructor(cacheSize) {
- this.kind = 'memoryLru';
- this._offlineComponentProvider = new LruGcMemoryOfflineComponentProvider(cacheSize);
- }
- toJSON() {
- return { kind: this.kind };
- }
- }
- /**
- * Creates an instance of `MemoryEagerGarbageCollector`. This is also the
- * default garbage collector unless it is explicitly specified otherwise.
- */
- function memoryEagerGarbageCollector() {
- return new MemoryEagerGabageCollectorImpl();
- }
- /**
- * Creates an instance of `MemoryLruGarbageCollector`.
- *
- * A target size can be specified as part of the setting parameter. The
- * collector will start deleting documents once the cache size exceeds
- * the given size. The default cache size is 40MB (40 * 1024 * 1024 bytes).
- */
- function memoryLruGarbageCollector(settings) {
- return new MemoryLruGabageCollectorImpl(settings === null || settings === void 0 ? void 0 : settings.cacheSizeBytes);
- }
- /**
- * Creates an instance of `MemoryLocalCache`. The instance can be set to
- * `FirestoreSettings.cache` to tell the SDK which cache layer to use.
- */
- function memoryLocalCache(settings) {
- return new MemoryLocalCacheImpl(settings);
- }
- /**
- * Creates an instance of `PersistentLocalCache`. The instance can be set to
- * `FirestoreSettings.cache` to tell the SDK which cache layer to use.
- *
- * Persistent cache cannot be used in a Node.js environment.
- */
- function persistentLocalCache(settings) {
- return new PersistentLocalCacheImpl(settings);
- }
- class SingleTabManagerImpl {
- constructor(forceOwnership) {
- this.forceOwnership = forceOwnership;
- this.kind = 'persistentSingleTab';
- }
- toJSON() {
- return { kind: this.kind };
- }
- /**
- * @internal
- */
- _initialize(settings) {
- this._onlineComponentProvider = new OnlineComponentProvider();
- this._offlineComponentProvider = new IndexedDbOfflineComponentProvider(this._onlineComponentProvider, settings === null || settings === void 0 ? void 0 : settings.cacheSizeBytes, this.forceOwnership);
- }
- }
- class MultiTabManagerImpl {
- constructor() {
- this.kind = 'PersistentMultipleTab';
- }
- toJSON() {
- return { kind: this.kind };
- }
- /**
- * @internal
- */
- _initialize(settings) {
- this._onlineComponentProvider = new OnlineComponentProvider();
- this._offlineComponentProvider = new MultiTabOfflineComponentProvider(this._onlineComponentProvider, settings === null || settings === void 0 ? void 0 : settings.cacheSizeBytes);
- }
- }
- /**
- * Creates an instance of `PersistentSingleTabManager`.
- *
- * @param settings Configures the created tab manager.
- */
- function persistentSingleTabManager(settings) {
- return new SingleTabManagerImpl(settings === null || settings === void 0 ? void 0 : settings.forceOwnership);
- }
- /**
- * Creates an instance of `PersistentMultipleTabManager`.
- */
- function persistentMultipleTabManager() {
- return new MultiTabManagerImpl();
- }
- /**
- * @license
- * Copyright 2022 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const DEFAULT_TRANSACTION_OPTIONS = {
- maxAttempts: 5
- };
- function validateTransactionOptions(options) {
- if (options.maxAttempts < 1) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Max attempts must be at least 1');
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * A write batch, used to perform multiple writes as a single atomic unit.
- *
- * A `WriteBatch` object can be acquired by calling {@link writeBatch}. It
- * provides methods for adding writes to the write batch. None of the writes
- * will be committed (or visible locally) until {@link WriteBatch.commit} is
- * called.
- */
- class WriteBatch {
- /** @hideconstructor */
- constructor(_firestore, _commitHandler) {
- this._firestore = _firestore;
- this._commitHandler = _commitHandler;
- this._mutations = [];
- this._committed = false;
- this._dataReader = newUserDataReader(_firestore);
- }
- set(documentRef, data, options) {
- this._verifyNotCommitted();
- const ref = validateReference(documentRef, this._firestore);
- const convertedValue = applyFirestoreDataConverter(ref.converter, data, options);
- const parsed = parseSetData(this._dataReader, 'WriteBatch.set', ref._key, convertedValue, ref.converter !== null, options);
- this._mutations.push(parsed.toMutation(ref._key, Precondition.none()));
- return this;
- }
- update(documentRef, fieldOrUpdateData, value, ...moreFieldsAndValues) {
- this._verifyNotCommitted();
- const ref = validateReference(documentRef, this._firestore);
- // For Compat types, we have to "extract" the underlying types before
- // performing validation.
- fieldOrUpdateData = util.getModularInstance(fieldOrUpdateData);
- let parsed;
- if (typeof fieldOrUpdateData === 'string' ||
- fieldOrUpdateData instanceof FieldPath) {
- parsed = parseUpdateVarargs(this._dataReader, 'WriteBatch.update', ref._key, fieldOrUpdateData, value, moreFieldsAndValues);
- }
- else {
- parsed = parseUpdateData(this._dataReader, 'WriteBatch.update', ref._key, fieldOrUpdateData);
- }
- this._mutations.push(parsed.toMutation(ref._key, Precondition.exists(true)));
- return this;
- }
- /**
- * Deletes the document referred to by the provided {@link DocumentReference}.
- *
- * @param documentRef - A reference to the document to be deleted.
- * @returns This `WriteBatch` instance. Used for chaining method calls.
- */
- delete(documentRef) {
- this._verifyNotCommitted();
- const ref = validateReference(documentRef, this._firestore);
- this._mutations = this._mutations.concat(new DeleteMutation(ref._key, Precondition.none()));
- return this;
- }
- /**
- * Commits all of the writes in this write batch as a single atomic unit.
- *
- * The result of these writes will only be reflected in document reads that
- * occur after the returned promise resolves. If the client is offline, the
- * write fails. If you would like to see local modifications or buffer writes
- * until the client is online, use the full Firestore SDK.
- *
- * @returns A `Promise` resolved once all of the writes in the batch have been
- * successfully written to the backend as an atomic unit (note that it won't
- * resolve while you're offline).
- */
- commit() {
- this._verifyNotCommitted();
- this._committed = true;
- if (this._mutations.length > 0) {
- return this._commitHandler(this._mutations);
- }
- return Promise.resolve();
- }
- _verifyNotCommitted() {
- if (this._committed) {
- throw new FirestoreError(Code.FAILED_PRECONDITION, 'A write batch can no longer be used after commit() ' +
- 'has been called.');
- }
- }
- }
- function validateReference(documentRef, firestore) {
- documentRef = util.getModularInstance(documentRef);
- if (documentRef.firestore !== firestore) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Provided document reference is from a different Firestore instance.');
- }
- else {
- return documentRef;
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- // TODO(mrschmidt) Consider using `BaseTransaction` as the base class in the
- // legacy SDK.
- /**
- * A reference to a transaction.
- *
- * The `Transaction` object passed to a transaction's `updateFunction` provides
- * the methods to read and write data within the transaction context. See
- * {@link runTransaction}.
- */
- class Transaction$1 {
- /** @hideconstructor */
- constructor(_firestore, _transaction) {
- this._firestore = _firestore;
- this._transaction = _transaction;
- this._dataReader = newUserDataReader(_firestore);
- }
- /**
- * Reads the document referenced by the provided {@link DocumentReference}.
- *
- * @param documentRef - A reference to the document to be read.
- * @returns A `DocumentSnapshot` with the read data.
- */
- get(documentRef) {
- const ref = validateReference(documentRef, this._firestore);
- const userDataWriter = new LiteUserDataWriter(this._firestore);
- return this._transaction.lookup([ref._key]).then(docs => {
- if (!docs || docs.length !== 1) {
- return fail();
- }
- const doc = docs[0];
- if (doc.isFoundDocument()) {
- return new DocumentSnapshot$1(this._firestore, userDataWriter, doc.key, doc, ref.converter);
- }
- else if (doc.isNoDocument()) {
- return new DocumentSnapshot$1(this._firestore, userDataWriter, ref._key, null, ref.converter);
- }
- else {
- throw fail();
- }
- });
- }
- set(documentRef, value, options) {
- const ref = validateReference(documentRef, this._firestore);
- const convertedValue = applyFirestoreDataConverter(ref.converter, value, options);
- const parsed = parseSetData(this._dataReader, 'Transaction.set', ref._key, convertedValue, ref.converter !== null, options);
- this._transaction.set(ref._key, parsed);
- return this;
- }
- update(documentRef, fieldOrUpdateData, value, ...moreFieldsAndValues) {
- const ref = validateReference(documentRef, this._firestore);
- // For Compat types, we have to "extract" the underlying types before
- // performing validation.
- fieldOrUpdateData = util.getModularInstance(fieldOrUpdateData);
- let parsed;
- if (typeof fieldOrUpdateData === 'string' ||
- fieldOrUpdateData instanceof FieldPath) {
- parsed = parseUpdateVarargs(this._dataReader, 'Transaction.update', ref._key, fieldOrUpdateData, value, moreFieldsAndValues);
- }
- else {
- parsed = parseUpdateData(this._dataReader, 'Transaction.update', ref._key, fieldOrUpdateData);
- }
- this._transaction.update(ref._key, parsed);
- return this;
- }
- /**
- * Deletes the document referred to by the provided {@link DocumentReference}.
- *
- * @param documentRef - A reference to the document to be deleted.
- * @returns This `Transaction` instance. Used for chaining method calls.
- */
- delete(documentRef) {
- const ref = validateReference(documentRef, this._firestore);
- this._transaction.delete(ref._key);
- return this;
- }
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * A reference to a transaction.
- *
- * The `Transaction` object passed to a transaction's `updateFunction` provides
- * the methods to read and write data within the transaction context. See
- * {@link runTransaction}.
- */
- class Transaction extends Transaction$1 {
- // This class implements the same logic as the Transaction API in the Lite SDK
- // but is subclassed in order to return its own DocumentSnapshot types.
- /** @hideconstructor */
- constructor(_firestore, _transaction) {
- super(_firestore, _transaction);
- this._firestore = _firestore;
- }
- /**
- * Reads the document referenced by the provided {@link DocumentReference}.
- *
- * @param documentRef - A reference to the document to be read.
- * @returns A `DocumentSnapshot` with the read data.
- */
- get(documentRef) {
- const ref = validateReference(documentRef, this._firestore);
- const userDataWriter = new ExpUserDataWriter(this._firestore);
- return super
- .get(documentRef)
- .then(liteDocumentSnapshot => new DocumentSnapshot(this._firestore, userDataWriter, ref._key, liteDocumentSnapshot._document, new SnapshotMetadata(
- /* hasPendingWrites= */ false,
- /* fromCache= */ false), ref.converter));
- }
- }
- /**
- * Executes the given `updateFunction` and then attempts to commit the changes
- * applied within the transaction. If any document read within the transaction
- * has changed, Cloud Firestore retries the `updateFunction`. If it fails to
- * commit after 5 attempts, the transaction fails.
- *
- * The maximum number of writes allowed in a single transaction is 500.
- *
- * @param firestore - A reference to the Firestore database to run this
- * transaction against.
- * @param updateFunction - The function to execute within the transaction
- * context.
- * @param options - An options object to configure maximum number of attempts to
- * commit.
- * @returns If the transaction completed successfully or was explicitly aborted
- * (the `updateFunction` returned a failed promise), the promise returned by the
- * `updateFunction `is returned here. Otherwise, if the transaction failed, a
- * rejected promise with the corresponding failure error is returned.
- */
- function runTransaction(firestore, updateFunction, options) {
- firestore = cast(firestore, Firestore);
- const optionsWithDefaults = Object.assign(Object.assign({}, DEFAULT_TRANSACTION_OPTIONS), options);
- validateTransactionOptions(optionsWithDefaults);
- const client = ensureFirestoreConfigured(firestore);
- return firestoreClientTransaction(client, internalTransaction => updateFunction(new Transaction(firestore, internalTransaction)), optionsWithDefaults);
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Returns a sentinel for use with {@link @firebase/firestore/lite#(updateDoc:1)} or
- * {@link @firebase/firestore/lite#(setDoc:1)} with `{merge: true}` to mark a field for deletion.
- */
- function deleteField() {
- return new DeleteFieldValueImpl('deleteField');
- }
- /**
- * Returns a sentinel used with {@link @firebase/firestore/lite#(setDoc:1)} or {@link @firebase/firestore/lite#(updateDoc:1)} to
- * include a server-generated timestamp in the written data.
- */
- function serverTimestamp() {
- return new ServerTimestampFieldValueImpl('serverTimestamp');
- }
- /**
- * Returns a special value that can be used with {@link @firebase/firestore/lite#(setDoc:1)} or {@link
- * @firebase/firestore/lite#(updateDoc:1)} that tells the server to union the given elements with any array
- * value that already exists on the server. Each specified element that doesn't
- * already exist in the array will be added to the end. If the field being
- * modified is not already an array it will be overwritten with an array
- * containing exactly the specified elements.
- *
- * @param elements - The elements to union into the array.
- * @returns The `FieldValue` sentinel for use in a call to `setDoc()` or
- * `updateDoc()`.
- */
- function arrayUnion(...elements) {
- // NOTE: We don't actually parse the data until it's used in set() or
- // update() since we'd need the Firestore instance to do this.
- return new ArrayUnionFieldValueImpl('arrayUnion', elements);
- }
- /**
- * Returns a special value that can be used with {@link (setDoc:1)} or {@link
- * updateDoc:1} that tells the server to remove the given elements from any
- * array value that already exists on the server. All instances of each element
- * specified will be removed from the array. If the field being modified is not
- * already an array it will be overwritten with an empty array.
- *
- * @param elements - The elements to remove from the array.
- * @returns The `FieldValue` sentinel for use in a call to `setDoc()` or
- * `updateDoc()`
- */
- function arrayRemove(...elements) {
- // NOTE: We don't actually parse the data until it's used in set() or
- // update() since we'd need the Firestore instance to do this.
- return new ArrayRemoveFieldValueImpl('arrayRemove', elements);
- }
- /**
- * Returns a special value that can be used with {@link @firebase/firestore/lite#(setDoc:1)} or {@link
- * @firebase/firestore/lite#(updateDoc:1)} that tells the server to increment the field's current value by
- * the given value.
- *
- * If either the operand or the current field value uses floating point
- * precision, all arithmetic follows IEEE 754 semantics. If both values are
- * integers, values outside of JavaScript's safe number range
- * (`Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER`) are also subject to
- * precision loss. Furthermore, once processed by the Firestore backend, all
- * integer operations are capped between -2^63 and 2^63-1.
- *
- * If the current field value is not of type `number`, or if the field does not
- * yet exist, the transformation sets the field to the given value.
- *
- * @param n - The value to increment by.
- * @returns The `FieldValue` sentinel for use in a call to `setDoc()` or
- * `updateDoc()`
- */
- function increment(n) {
- return new NumericIncrementFieldValueImpl('increment', n);
- }
- /**
- * @license
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Creates a write batch, used for performing multiple writes as a single
- * atomic operation. The maximum number of writes allowed in a single {@link WriteBatch}
- * is 500.
- *
- * Unlike transactions, write batches are persisted offline and therefore are
- * preferable when you don't need to condition your writes on read data.
- *
- * @returns A {@link WriteBatch} that can be used to atomically execute multiple
- * writes.
- */
- function writeBatch(firestore) {
- firestore = cast(firestore, Firestore);
- ensureFirestoreConfigured(firestore);
- return new WriteBatch(firestore, mutations => executeWrite(firestore, mutations));
- }
- /**
- * @license
- * Copyright 2021 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- function setIndexConfiguration(firestore, jsonOrConfiguration) {
- var _a;
- firestore = cast(firestore, Firestore);
- const client = ensureFirestoreConfigured(firestore);
- if (!client._uninitializedComponentsProvider ||
- ((_a = client._uninitializedComponentsProvider) === null || _a === void 0 ? void 0 : _a._offlineKind) === 'memory') {
- // PORTING NOTE: We don't return an error if the user has not enabled
- // persistence since `enableIndexeddbPersistence()` can fail on the Web.
- logWarn('Cannot enable indexes when persistence is disabled');
- return Promise.resolve();
- }
- const parsedIndexes = parseIndexes(jsonOrConfiguration);
- return firestoreClientSetIndexConfiguration(client, parsedIndexes);
- }
- function parseIndexes(jsonOrConfiguration) {
- const indexConfiguration = typeof jsonOrConfiguration === 'string'
- ? tryParseJson(jsonOrConfiguration)
- : jsonOrConfiguration;
- const parsedIndexes = [];
- if (Array.isArray(indexConfiguration.indexes)) {
- for (const index of indexConfiguration.indexes) {
- const collectionGroup = tryGetString(index, 'collectionGroup');
- const segments = [];
- if (Array.isArray(index.fields)) {
- for (const field of index.fields) {
- const fieldPathString = tryGetString(field, 'fieldPath');
- const fieldPath = fieldPathFromDotSeparatedString('setIndexConfiguration', fieldPathString);
- if (field.arrayConfig === 'CONTAINS') {
- segments.push(new IndexSegment(fieldPath, 2 /* IndexKind.CONTAINS */));
- }
- else if (field.order === 'ASCENDING') {
- segments.push(new IndexSegment(fieldPath, 0 /* IndexKind.ASCENDING */));
- }
- else if (field.order === 'DESCENDING') {
- segments.push(new IndexSegment(fieldPath, 1 /* IndexKind.DESCENDING */));
- }
- }
- }
- parsedIndexes.push(new FieldIndex(FieldIndex.UNKNOWN_ID, collectionGroup, segments, IndexState.empty()));
- }
- }
- return parsedIndexes;
- }
- function tryParseJson(json) {
- try {
- return JSON.parse(json);
- }
- catch (e) {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Failed to parse JSON: ' + (e === null || e === void 0 ? void 0 : e.message));
- }
- }
- function tryGetString(data, property) {
- if (typeof data[property] !== 'string') {
- throw new FirestoreError(Code.INVALID_ARGUMENT, 'Missing string value for: ' + property);
- }
- return data[property];
- }
- /**
- * @license
- * Copyright 2021 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- registerFirestore('node');
- exports.AbstractUserDataWriter = AbstractUserDataWriter;
- exports.AggregateField = AggregateField;
- exports.AggregateQuerySnapshot = AggregateQuerySnapshot;
- exports.Bytes = Bytes;
- exports.CACHE_SIZE_UNLIMITED = CACHE_SIZE_UNLIMITED;
- exports.CollectionReference = CollectionReference;
- exports.DocumentReference = DocumentReference;
- exports.DocumentSnapshot = DocumentSnapshot;
- exports.FieldPath = FieldPath;
- exports.FieldValue = FieldValue;
- exports.Firestore = Firestore;
- exports.FirestoreError = FirestoreError;
- exports.GeoPoint = GeoPoint;
- exports.LoadBundleTask = LoadBundleTask;
- exports.Query = Query;
- exports.QueryCompositeFilterConstraint = QueryCompositeFilterConstraint;
- exports.QueryConstraint = QueryConstraint;
- exports.QueryDocumentSnapshot = QueryDocumentSnapshot;
- exports.QueryEndAtConstraint = QueryEndAtConstraint;
- exports.QueryFieldFilterConstraint = QueryFieldFilterConstraint;
- exports.QueryLimitConstraint = QueryLimitConstraint;
- exports.QueryOrderByConstraint = QueryOrderByConstraint;
- exports.QuerySnapshot = QuerySnapshot;
- exports.QueryStartAtConstraint = QueryStartAtConstraint;
- exports.SnapshotMetadata = SnapshotMetadata;
- exports.Timestamp = Timestamp;
- exports.Transaction = Transaction;
- exports.WriteBatch = WriteBatch;
- exports._DatabaseId = DatabaseId;
- exports._DocumentKey = DocumentKey;
- exports._EmptyAppCheckTokenProvider = EmptyAppCheckTokenProvider;
- exports._EmptyAuthCredentialsProvider = EmptyAuthCredentialsProvider;
- exports._FieldPath = FieldPath$1;
- exports._TestingHooks = TestingHooks;
- exports._cast = cast;
- exports._debugAssert = debugAssert;
- exports._isBase64Available = isBase64Available;
- exports._logWarn = logWarn;
- exports._validateIsNotUsedTogether = validateIsNotUsedTogether;
- exports.addDoc = addDoc;
- exports.aggregateFieldEqual = aggregateFieldEqual;
- exports.aggregateQuerySnapshotEqual = aggregateQuerySnapshotEqual;
- exports.and = and;
- exports.arrayRemove = arrayRemove;
- exports.arrayUnion = arrayUnion;
- exports.average = average;
- exports.clearIndexedDbPersistence = clearIndexedDbPersistence;
- exports.collection = collection;
- exports.collectionGroup = collectionGroup;
- exports.connectFirestoreEmulator = connectFirestoreEmulator;
- exports.count = count;
- exports.deleteDoc = deleteDoc;
- exports.deleteField = deleteField;
- exports.disableNetwork = disableNetwork;
- exports.doc = doc;
- exports.documentId = documentId;
- exports.enableIndexedDbPersistence = enableIndexedDbPersistence;
- exports.enableMultiTabIndexedDbPersistence = enableMultiTabIndexedDbPersistence;
- exports.enableNetwork = enableNetwork;
- exports.endAt = endAt;
- exports.endBefore = endBefore;
- exports.ensureFirestoreConfigured = ensureFirestoreConfigured;
- exports.executeWrite = executeWrite;
- exports.getAggregateFromServer = getAggregateFromServer;
- exports.getCountFromServer = getCountFromServer;
- exports.getDoc = getDoc;
- exports.getDocFromCache = getDocFromCache;
- exports.getDocFromServer = getDocFromServer;
- exports.getDocs = getDocs;
- exports.getDocsFromCache = getDocsFromCache;
- exports.getDocsFromServer = getDocsFromServer;
- exports.getFirestore = getFirestore;
- exports.increment = increment;
- exports.initializeFirestore = initializeFirestore;
- exports.limit = limit;
- exports.limitToLast = limitToLast;
- exports.loadBundle = loadBundle;
- exports.memoryEagerGarbageCollector = memoryEagerGarbageCollector;
- exports.memoryLocalCache = memoryLocalCache;
- exports.memoryLruGarbageCollector = memoryLruGarbageCollector;
- exports.namedQuery = namedQuery;
- exports.onSnapshot = onSnapshot;
- exports.onSnapshotsInSync = onSnapshotsInSync;
- exports.or = or;
- exports.orderBy = orderBy;
- exports.persistentLocalCache = persistentLocalCache;
- exports.persistentMultipleTabManager = persistentMultipleTabManager;
- exports.persistentSingleTabManager = persistentSingleTabManager;
- exports.query = query;
- exports.queryEqual = queryEqual;
- exports.refEqual = refEqual;
- exports.runTransaction = runTransaction;
- exports.serverTimestamp = serverTimestamp;
- exports.setDoc = setDoc;
- exports.setIndexConfiguration = setIndexConfiguration;
- exports.setLogLevel = setLogLevel;
- exports.snapshotEqual = snapshotEqual;
- exports.startAfter = startAfter;
- exports.startAt = startAt;
- exports.sum = sum;
- exports.terminate = terminate;
- exports.updateDoc = updateDoc;
- exports.waitForPendingWrites = waitForPendingWrites;
- exports.where = where;
- exports.writeBatch = writeBatch;
- //# sourceMappingURL=index.node.cjs.js.map
|