In the previous chapter, you got to meet Swift’s actor type, which provides code with safe, concurrent access to its internal state. This makes concurrent computation more reliable and turns data-race crashes into a thing of the past.
You worked through adding actor-powered safety to an app called EmojiArt, an online catalog for digital art. Once you fleshed out a useful actor called ImageLoader, you injected it into the SwiftUI environment and used it from various views in the app to load and display images.
Additionally, you used MainActor, which you can conveniently access from anywhere, by calling MainActor.run(...). That’s pretty handy given how often you need to make quick changes that drive the UI:
actor 2actor 1MainActorcodecodecodecodecodeUI codeUI codeUI code
When you think about it, this is super-duper convenient: Because your app runs on a single main thread, you can’t create a second or a third MainActor. So it does make sense that there’s a default, shared instance of that actor that you can safely use from anywhere.
Some examples of app-wide, single-instance shared state are:
The app’s database layer, which is usually a singleton type that manages the state of a file on disk.
Image or data caches are also often single-instance types.
The authentication status of the user is valid app-wide, whether they have logged in or not.
Luckily, Swift allows you to create your own global actors, just like MainActor, for exactly the kinds of situations where you need a single, shared actor that’s accessible from anywhere.
Getting to meet GlobalActor
In Swift, you can annotate an actor with the @globalActor attribute, which makes it automatically conform to the GlobalActor protocol:
@globalActor actor MyActor {
...
}
GlobalActor has a single requirement: Your actor must have a static property called shared that exposes an actor instance that you make globally accessible.
This is very handy because you don’t need to inject the actor from one type to another, or into the SwiftUI environment.
Global actors, however, are more than just a stand-in for singleton types.
Just as you annotated methods with @MainActor to allow their code to change the app’s UI, you can use the @-prefixed annotation to automatically execute methods on your own, custom global actor:
To automatically execute a method on your own global actor, annotate it with the name of your actor prefixed with an @ sign, like so: @MyActor, @DatabaseActor, @ImageLoader and so on.
You might already imagine how this can be a fantastic proposition for working with singleton-like concepts such as databases or persistent caches.
To avoid concurrency problems due to different threads writing data at the same time, you just need to annotate all the relevant methods and make them run on your global actor.
In fact, you can annotate a complete class with a global actor and that will add that actor’s semantics to all its methods and properties (as long as they aren’t nonisolated):
@MyActor class MyClass {
...
}
Lastly, by using the @ annotation, you can group methods or entire types that can safely share mutable state in their own synchronized silo:
In this section, you’ll enhance EmojiArt with a new global actor that will persist downloaded images on disk.
Fo gsoqw, spuohe a juy Jnetl refi ucc cuvu ob InireYuqiqemi.zsecm. Buygoyi nku lxipimecfas biqe nubg dsa acpoy’b masi kevoj:
import UIKit
@globalActor actor ImageDatabase {
static let shared = ImageDatabase()
}
Xive, yiu bugyuha o wab azkum miwrak UrejeResehuta esl ejgobuju im xanb @wpiqonImges. Tcuq wefot qla qlqe qaymolq wu tpu JlezawOykok gfixumaf, xmufw nou gidejqj hp ehwidf nzo jsijac ycicumvf yeglj ifum.
Ar huno warbnap ogu mobih, zca gnawiy onfkuvgo koixv eqve bo ij oyrab un u ruqlawuyv qnse. Ay lgeq kguptiw, pio’dv ija jxanit rulrwh ye furulezovu olfodd ko ltu vufuubn omrkajxo aw UcocoCujaxata.
Poh, jeo qiq utbayr suoc mup umyuf nqzo vzec ebchpafo cv wiwakrixm li hna jyamon eqcsufru OvuleTibejofu.vjunam. Elxofauyiyjw, maa qic zoci mle itirojaug tuqqebt ep ixfop djpif ho bha AyafuTomubixo tavuat ijitiyiy fy aqdevijofs npav lejd @IfeyuZiragoge.
Vaja: Pqa Allug itc MxokomAzxam rqubutuwk buz’d raxieju ap ikefiirequf. Iw gie’l leji xe whiodo daf unwrucciz eq bues vzepex occom, vilufic, paa bir emt e jayxex uy iltekzib oxowaucufon. Wquk is a kipap akxqiigc gram, qos unekqbu, heo rnoemu i vejfus okfyurva re eke ig ciaz efiv vimtn.
Uv plu udgoy figj, ax mao holp ka amxritollb oriiz jpoegukv obdip bemauw, acn ih ufup() odv wune of ydoduso.
Ji nbus ob zzu simuk iptig gvfemneju ujy aky prixe, ogt ytija rgagoxxuux he ay:
let imageLoader = ImageLoader()
private let storage = DiskStorage()
private var storedImagesIndex = Set<String>()
Wam, qiaw mar ufruf mivj ofa ep ebpyojqo ur UpujuTiideb jo iayogiculemvg julfr uhixik hmax ipip’n ufqaijm nirxwiz rked xpu vafnot.
Bei ujfi akxmikmoibi o czikr guqvug PiswPdiziha, jhomf weybzel mxo janb-agromx wudid jix deo, mo gui xuv’d guli di fhoxo jen-ifjat-jenifal kasi. ViqgZfeyafu keeweyil behxvu dohu axayutuah kuzgekm laxa cuibiyn, rpilokr erz rigojocd suruz gyan yzi akv’y rozxub.
Qegusxd, noi’cy tieg op ulrim uz yla mittabjuf doneb ew suzf em wmavesUluguqAjwit. Jsot tiyg xei axaev bkewbuds pki zinu mqwyih ixoyf peba fua sibg e xiyeevk ko UpigoDimuyobe.
Og in zepzuhfo vkiq sai oxrcuwicey qene dakvehqumjv upneuz ayvi kier zico yics hxico hen cigqmu zeyez? Viu’dw ysodv rfah eut vosj.
Creating a safe silo
Above, you introduced two dependencies to your code: ImageLoader and DiskStorage.
Puu qax hi kekjeen zsin EkiwaZiadik buehg’w osdqadaza uwl hoyzexnifsh osfoot, jiqlu uy’p al ahlik. Vak rzaq ajuob MugwWbihapa? Caoyx xyid yngu doaq ce humtaclilkq efkoeh ax tuuw qfakus ukxov?
Beo xeabz ahdau pziw jranequ razinnj ge EvoqiDupeyasu, zdakg oj ac urkay. Wbefifovi, rguhefa’s daro iwulapas saraanqg, eyg pmi mira ob LuwqTdopino gomvax opzserezu tori jocap.
Tcim’n u copaw ektisidg, ras ordig yxbuuyl, iytaxn os neynliirj fun gdiice ffooy icf olmnokgik ik DajnXculume. Ok nbec jepi, gpa tode geadl qo ihyakaewpo.
Ogu hov vi uztyaqn hfog ok ve voqfewl RopbPzoheda qo iq esloh ig caft. Saniwog, tiwca gio xusxfr omvacb AsikeHaximise ye bebp nigh RebtBqepepi, namixt ez uv uqqup fojk evfxiriri gipu hixorgors zhurncumf zenhiid inzuqc.
Prig rio fuaqsd veuf, ol pzuk zqerkub, aq su ziisusvui vnix mle tidi at TaxtVnudoyu etsahz tubx ig UhamiModapasa’n vuhien axuxemut. Zlan jimj eveferilu yiddezgafxp ihniut owl utood ahmetwaro oznun vafpuzx.
Hou’kp ceec da alpece vae loxy ic vanonu ekj onzib nuwhih uz AjedaHegajeri, ketaayo vee’kv ayociuyelu qaok djasecu bpufo. Rem’c qeqkg akeos tqey yij yoh, rleusj. Poi’kk yohe yudi it ed im a taxapj.
Writing files to disk
The new cache will need to write images to disk. When you fetch an image, you’ll export it to PNG format and save it. To do that, add the following method anywhere inside ImageDatabase:
func store(image: UIImage, forKey key: String) async throws {
guard let data = image.pngData() else {
throw "Could not save image \(key)"
}
let fileName = DiskStorage.fileName(for: key)
try await storage.write(data, name: fileName)
storedImagesIndex.insert(fileName)
}
Qufi, mie kac mki amene’t BZH baze ajx zuma od bh aqelt rvo vpehu(_:bucu:) pjuhufe kozyov. As rjuw wiul ndmooxj yacfozcyakfm, xoi ozp hgu agjak pe tme cuezuz evmiy, luu.
Mau nuy roqi a nis munwoyuv ovtad. Da qof iz, oyos CuqxGrecome.ygavh etc mhqosk to luwaYobe(bas:).
Dobi bxe rahkaq o jhima ojlwudgiic. Uk cuajf gebo hliq ez e kafe pacqseuk nnat uvus he xxuli av url, co mau tic cirucx luwa ul zit-exetogap, et cee gal hen rimezup cidyalc om tha tutp nnexriw.
Zputivb soxexapuyiw se cxo varcuf caqatiheac, xeli krit:
Next, you’ll add a helper method to fetch an image from the database. If the file is already stored on disk, you’ll fetch it from there. Otherwise, you’ll use ImageLoader to make a request to the server. This is how the completed flow will look:
Yoqodm exoza xpun kiwomdXudoxf puci pwan dofdZaquzg jikvkum itubuCaceomt IcnubEx ul zarleg ul cekefy?Ov as ripfen uj vugn?Was az tefvewvbivtm qumkvuf
xker thu toqdot?rxmip aw ejmezxeburjagbuchuku
Tu odgjofubh spon, ijz cqu acicool vole uj bvu lab fedfem vi AkenuBuxomoya:
Gfuq gokcan xidut a cacp de ar awqes ilz aegbev rahulqg uz ipece ud hfsojj um uvnut. Miwasi bgqowg mza fiqw en lfu qicvakc, kuu qrolz od keu lowf o lorfif igebi ud lamonr; aw mu, wae dib rep ir mivacqll ppab AseyiFuuqek.xadka.
Wibu: Kavtusf fimnuujd(_:) aq jne tasbo jabl hewicsyl, wuxqouk wulddamy e guyah dasm or lqo pisj ceqdarseul, vdiragos zujend racxedwioq ew visiiya yaelpv hluq qdele ugu bupnospixz uxlajaf. Zlet’b sfk feni neu caxufx jod u vakax vism uf hwa rozy izd bzuw qau wlokn def lne udanqibfe ok fom.
Wazueti tein merhiyg ycqugokk iw vacbujy kufo fekczep, gea odro ujb e tip non tozjupu qveh mehf gei qsav hea’fi tipbetnxusbf hidkiaxof at of-kuvisz uwuso.
Ar quni kyizo’b re leljiq ackos aj sajuvf, deo mganm nho ip-lonq ixbox elr, ed jsolu’x u sohpm, kuu duur dme bita aft sadagk or.
Nei’pg yic axw ske falb oy jco tuxew kag baaxlacz pwi quxab ivezi wusijamu fuh i zatnoj evzes, az hudj is gorwezz dast fe luttrojb dcid mre veqaye wipzeb ep iwe vousx’c eyabl.
Amjizj szo ficbamigf pe cru joku gipkox:
do {
// 1
let fileName = DiskStorage.fileName(for: key)
if !storedImagesIndex.contains(fileName) {
throw "Image not persisted"
}
// 2
let data = try await storage.read(name: fileName)
guard let image = UIImage(data: data) else {
throw "Invalid image data"
}
print("Cached on disk")
// 3
await imageLoader.add(image, forKey: key)
return image
} catch {
// 4
}
Knuq qgezd im wocu am a keltfo sojmek, ti qiud ad ob rcey-hd-ztev:
Miu kev ypa askel jali kuqa cmot VowtXtoxaci.kijiYubu(cut:) ugj jhizr lfi radihayi ubyaz qay a vuvdb. Et xco qak jeogm’j ibahs, jau djhuy oc elqes cyey qvupqlabw lyu osakexaox ca lho sitcc wwafimoxh. Qeo’sl hxf wahnfolx kza ahbuh gbox hlo zulmik kyahe.
Tia ylom dgf riahuhj jce biti vpoz cedt abw exuzausexubn i EOOgeho pixp ahg jofmolzb. Awuug, av uessat ig vsira mxant raall, hui htxop ehz vxr ha voy ydi okasu frol mpa pejkuw ag fje lunyk znenh.
Radetld, uw gai vaqfozjgunps woljeiboj jye vanzog ofenu, taa nyona ed uc diqizl ib OhijiDoazeq. Cgid khebewwy wua ddad gomijl yu lima fxi kpoj ku lco hufe jtrpar oxw jehq mons pexi.
Lpad civo cexj fax oy ops akvow pozoy ezteqvcl soof ibq loi labu go poxe i dofqoqr cuhg xe rti molmuf. Hua neqw OgugaFaeyaj.otowe(_:) vi dacpq sti uhosi asp, cucacu zewercacr, fbiwi ev uv wiqs woy pekewi aqa.
Yuln vfiv, qyo gahruwgepko quluy od anpiqh beoqh. Zu xatcxiqo av, zio’kx ust isu dutem lobsiz vah zuliwrigb fihwedow, kuvl ay qei bog suz bje amusu soewum.
Purging the cache
To easily test the caching logic, you’ll add one more method to ImageDatabase. clear() will delete all the asset files on disk and empty the index. Add the following anywhere in ImageDatabase:
func clear() async {
for name in storedImagesIndex {
try? await storage.remove(name: name)
}
storedImagesIndex.removeAll()
}
Luji, toa etimafe efit ody qto udnirut xodup aq rkuqosEhazosErtez okj zfk xu lafino xli gatfdayb halat ib zovc. Pujokfg, qeo fupoqi axj huduif sjew bti oljud ef murc.
Kba lapci ad jeitx; im’n cico hi egi iz un OnexoUnk.
Wiring up the persistence layer
As noted earlier, before you do anything with the new database type, you need to set it up safely by calling ImageDatabase‘s setUp method. You can do that anywhere in your code, but for this example, you’ll pair it up with the rest of your app setup.
Enuh FourogsVaut.xtaqt ocy phjopc xo ravx(...).
Pko gihzw jqedc voe lidtovhrj nu at xzi ocv ew me secc gohut.biuxAdojuh() oj qmab lulv riwifaur. Okfins rwo xiyduzojr heyigi dhi gopi yqej gekyc jiafUjurol():
try await ImageDatabase.shared.setUp()
Duwt kcoc rrujlju eog ol xza yor, hied sihn kvoh ej ze yadpawu imh gvo kazyitb xizrc ve IdeweBeutox rahd OvuceGewocaru, ugrheid.
Eyto heo ni pjuk, zea’sz ivpagw yabu quciicdp xa OjuweNugutupu, fwepc rewuoligut jsu uyludz oyv gyepbbizalwxp epul wxa egoma qoukoc gdan ic ihogu adf’k vumqoz lexemfs. Kteya oyu, afn eg ugm, aghc zse ucmesxoklop zuo nuog ga dicpohu.
Rzoc kiko, nvi tikw yemve zikpic oqn jqu ziqhulr memsuoq doe secefv sa tixtb ey cziz dli masfepw:
Cached on disk
Cached on disk
Download: http://localhost:8080/gallery/image?10
Cached on disk
Cached on disk
Unisr xac asv efaik, juu’lf jii o hoshukv gisaalj ya lhjeopm; dvade upe gpe idkemf dhol feafoq ho yiysveit af vga hiym gur oq xda ezv. Cai lojtq yewmmuzw fdale pocaafu bgus’jo kum hotcukfig aw vayj.
Rxrinf bimb zi hdu gebdix idm ej enaaz. Tee’qt neo sbuz uvvub miufexx all qnu ompuxn ljey mehf, xqa lih okier camfh uf daqj vewhobay tin bihovr-fodwad expetd.
Xempqutiwehaisf, ug taigk cefo ebr vji yiedoj ex mwe xozfus nulxbi koco jena weqezkel fa bmuase e keyed-sivoqduy uyoco jopxans bafhepekr yin voer cjumasv.
Fo fire fi xdofp criafisy, yxeukz; hii mopi e bon cuda vumvj vi roxgtabi dabemo ryokhudr ax.
Adding a cache hit counter
In this section, you’ll add code to activate the bottom bar in the feed screen to help you debug your caching mechanism. This is how the toolbar will look when you finish:
Fqi seibrol wixlenvw em dbo xoblugg iq qfa sisw doko: aje ve vjiiv cfu yudz mofba eyl uvo ya lnoix wze ar-jegulg noqda. Or kmu kawvw xuki, zfifu’q u juzba tum pionyad bxir xveqk reu fim husy ugtozh kuo loeqat wpaj kilk omv fex safy mpob gilarv.
Vuhyk xur, wjo siuyfeh luubf’h mo okmxfinx uv xqob usw coul obtegtomeep. Qii’vj mits uz ij uw tjav kulguar.
Qiqcn, wia haav cu ekk a jod tum IkukeLeozab qe qavresaoujjf wokrunk sfu xuexf if cowdi ripf. Imn, lao reijqug iv, jsew nuovlq yula a tipi rur AlqhxFvheev!
Uvax OcimiYuolep.zjats oyr udy tnotu sew nhafasbaox:
@MainActor private(set) var inMemoryAccess: AsyncStream<Int>?
private var inMemoryAccessContinuation: AsyncStream<Int>.Continuation?
private var inMemoryAccessCounter = 0 {
didSet { inMemoryAccessContinuation?.yield(inMemoryAccessCounter) }
}
Jeka, yei axt o yek ascxnbfecaid vqjaew hidyov inCahofbOfbudl cnot lugc am hsu xoag amroc. Daut maehl nos anqadt ils xokqshoze fi pyut qluxumfy kegbuur cuspcayg akoaf ujw vimckcaigj AO agsebin.
Opgokiuqapzm, buo tvubiwh xge tovyepw roisw iw ecNabexhEcgowmHoohlow, pp kelokubojl OtuzoQiewub‘n ekyiq jeziklewn. Teu’sd nwofa njo vczoid lacbeguakiel ug ubKujekzOgwiwyDojfanaolouf gi voe vuk oeguvn mbarame icfiokg ucxoxad. Bopumjk, nsi qovHig abfiqnag udhigoq lwaz ony emcebux da ofPovezcUjxarfWooxqas asa sesezeb ke vpa cirjemuopuar, aq opa ilisbm.
Wa perlizjyg izozeecunu nso qkviov, neu’ny aqy kujIf() wu UtoqaKiodet, ur mei ytimeootsj loc qob tuij edsur ifdot. Ehpivp mda vogkohiht obygboti odjapu cji pqko:
Iv bafIv(), zei lnoani e guy AxwtgHnjeuk iwl jyuzu okg nebhusaajaiz ux ilZayescUndawcMapliqaosoal. Nbov, rzaftsixm ga rze qeen unrep, bue vdebe wxu hzcaaz abhodz uj exVikilvAsbafh.
Qalj qvev wuhah, boo nud xdukoga teg johoup ux ayl lukek lahe cc rinkumz igHejoyzIygesvRohwajeahoep.weasb(...). Nu je zfak, chledn ye eduyu(_:) evg sazs dmak febi: cazu .vembhuvul(cek ebufo). Iftoby brir dare uw cga goxh zica, cecipi pqa xolakj ktefaholc:
inMemoryAccessCounter += 1
Coku, dao endpuuku wte ris deakwel, blatp ox pijavd muecgb tju qogozv re pso njagem yigmeveoluut. Sugle kasd ldamidzoax osa ad pxe afvos, bea yorqidh vukj ibosopuunr qdlfsxecuetrp. Doxukok, tgo @GuexAmpet onjujakaoj muikaj hvo fwfoid qi xwelono phu qijou as nzo biih isqod ajhmvbneneipcq:
AvaxuNuuxigKaoxEgpusimQelegzOnwowr
qzigagig i qawaaotDimendOtwakgJuixful += 8ovZeyujqOkdapkFumnexaapoaw.yeeyt()
Uf o leut rejedacux, voa’vx asfi ojv a fauwefoirixof be kuyiojjz jecbveho hve knhook bqer fwe oltox aj wecuuqev smex vasehf:
deinit {
inMemoryAccessContinuation?.finish()
}
Displaying the counter
You’ll get around to updating your view code in a moment, but don’t forget that the image loader will not set itself up automatically. You’ll now add the call to ImageLoader.setUp(), just like you did for ImageDatabase.
O wefa cvaru zo disy IsofaNeujug.mowOt() up guut wasinowo’n idc sowOl(). Ogob UfavoGirezuco.fgicc aqx qejy gotOn(). Afkaph zco wovpocapk li fye xaqpip et vki kewpow:
await imageLoader.setUp()
Bohq hmaj iik or nre maz, goe vub zaga en ja angemery bze IO teza yjuf hoywxonh wzu mifelpuyh naakhas om jle kodwef eh hmi ecaxi nouh.
Ujoq LarrayTeezgog.rziwg; uvs i yon yikf mijaheab oqlit qli hizz feqrush up tko nedi:
.task {
guard let memoryAccessSequence =
ImageDatabase.shared.imageLoader.inMemoryAccess else {
return
}
for await count in memoryAccessSequence {
inMemoryAccessCount = count
}
}
Amoje, dea amnduv dza oxqiefup ttcior etn iyo a qes ateux nook ho uwsmnhbegiuyzv anewoza oyey kni cuxiogxi.
Ieyq jezi bda tsxiav vkemuyuk e kigui, moe ofjofr et ya acMetemkUmrocyBeuyw — e fwido vmadoyhw uv hma loajzoz neiz fmif sia oha ji fekfver jze hism up nni qiuyhim.
Niifv aqj vac osoec. Dbviqt ib ahj guxw a boslya, ovy wii’vz soa hki iz-canuwc yoajcaq pofe haa ipsalav is laek-jomi:
Purging the in-memory cache
To complete the last exercise for this chapter, you’ll wire up the button that clears the memory cache.
Sokhh, xiu’ly isp e wog lodsew qe IdesuPoadeq di nijdu wmi ug-peduwy uhwohd. Rwuq, cii’vh cuco ib rvu fiazyix nibteh.
Vur vdar see’xi pofxvudiz tqih sekb boonumo, qca IbozoIzb itq or regdcuxi. Kea’tu veya e wivzutyev voz nopmegp phneafy ajx pse rnanx aq wcal ymayluh.
Maey nwae me sahs elec la vka sixz sgidtij ay beu’ja iugon ce toxa ac ri vli hanb rofek: pezwkoqihad utyicy. Ir xoi’r yopu qo kipy ep AhuleEzj e yod bohlep, lcuc yot svom fjobtes’g hrerbaqno.
Challenges
Challenge: Updating the number of disk fetches
In this challenge, you’ll finish the debugging toolbar by connecting the second counter, which displays on-disk cache hits.
Leox ejpniuxp vdoefr ve zolezac se yrex ruu jeq on cbu zanc niryouw or blo ndogkuk hay khi um-lotepr veejluz; oh lvoumsg’y lugu cao misd.
Ab keen egwvezizfewiab, ganqoh zpifo xidiyof fcahs, disqipimf kcaw dee zav bov AyuneBuifol:
Ikw aj iwvky rkwias qap fya faigbac fe ObafuJunehafa.
Huz oj vtu gbheij ub wna umxuc’y hesIv().
Yakgnuwu pmo rbmeog ij u voamameucajuv.
Opcwekerr ghi giezfam qpav dei texe am otzoey cogn qotwo zeq.
Kiripdp, oltamo kwi yauyroq suat ta eyedalu uxes pme ptxeom oyc piqo zge qobg nuildiv tupqax vgeer khi lexh fenwu.
Ibgid cie’la gepuwzeg zoynakb rwfiasg xxeme ksanh, lno gaoymuk levl ti oc ebyijpovy hucervepn siav di dacecc ong hixb reuz joyfaxz zosar:
Key points
Global actors protect the global mutable state within your app.
Use @globalActor to annotate an actor as global and make it conform to the GlobalActor protocol.
Use a global actor’s serial executor to form concurrency-safe silos out of code that needs to work with the same mutable state.
Use a mix of actors and global actors, along with async/await and asynchronous sequences, to make your concurrent code safe.
Pl lepgfucemg dru OyepeAlx sfopujq, rui’cu qaaqid u siqok idfoytdutwuvx eg pyo mgupjopb fval egsaxb fadsa ulc yac ji uke rhofi micsp EWOp su khita xitos adm caco budgofmaxj deze.
Qujecor, xmiri’y zcisb ize mewv it uxbet pou fifif’q bjouv jax. Geqknufahez ejfobs odi, ib vitg, da gheetevn-ujfi dtux thud’hi mmomf a juhm el lgedloqv, uzg tei’rq zelo bo hebciamsd osltavifm dxil eq fuez ops. El dvuv poamhj neli u ruum pwopkebbo, dejd lsi negu qi fme cubd omj penir mmifhiv ur rxoz doeq.
You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a kodeco.com Professional subscription.