diff --git a/Guide/database-migrations.markdown b/Guide/database-migrations.markdown index fa65e2b8a..692ef2f80 100644 --- a/Guide/database-migrations.markdown +++ b/Guide/database-migrations.markdown @@ -73,6 +73,20 @@ migrate A good value for `MINIMUM_REVISION` is typically the unix timestamp of the time when the database was initially created. + +### IHP MIGRATIONS DIR + +In production when running the migrations binary it is sometimes convenient to have all your Migrations in a non-standard place: +e.g. if you need to push migrations onto production server without rebuilding the application. There is an Environment variable +`IHP_MIGRATION_DIR` to accomplish this. + +``` +IHP_MIGRATION_DIR=path/to/my/migration/dir +``` + +This can be set in the environment attribute set of your IHP app flake. + + ## Common Issues ### ALTER TYPE ... ADD cannot run inside a transaction block diff --git a/Guide/database.markdown b/Guide/database.markdown index ed1c145c6..f12ed6ebc 100644 --- a/Guide/database.markdown +++ b/Guide/database.markdown @@ -20,7 +20,7 @@ When the development server is running, you can connect to it via `postgresql:// When the development server is running, you can use your favorite UI tool (e.g. [TablePlus](https://tableplus.com/)) that allows connecting to Postgres. To do that you would need the following credentials: -Database Host: This is the application root + "/build/db". Use this command on terminal form the root of you app and copy the output: +Database Host: This is the application root + "/build/db". Use this command on terminal from the root of your app and copy the output: ``` echo `pwd`/build/db ``` diff --git a/Guide/jobs.markdown b/Guide/jobs.markdown index 511ebc595..e67009311 100644 --- a/Guide/jobs.markdown +++ b/Guide/jobs.markdown @@ -44,11 +44,25 @@ instance Job EmailCustomersJob where ### Running the job -IHP watches the job table in the database for any new records and automatically runs the job asynchronously when a new job is added. So to run a job, simply create a new record: +IHP watches the job table in the database for any new records and automatically runs the job asynchronously when a new job is added. There are two ways to run a job: + +1. Run immediately (as soon as a job worker is available): ```haskell newRecord @EmailCustomersJob |> create ``` +2. Schedule for future execution: + +```haskell +import Data.Time.Clock (addUTCTime, getCurrentTime, nominalDay) + +now <- getCurrentTime +newRecord @EmailCustomersJob + |> set #runAt (addUTCTime nominalDay now) -- Schedule 24 hours in the future + |> create +``` + +The `runAt` field determines when the job should be executed. If not set, the job runs immediately. When set, IHP polls for scheduled jobs approximately every minute and executes any jobs whose `runAt` time has passed. This can be done in a controller action or in a script as will be shown below. diff --git a/IHP/DataSync/ControllerImpl.hs b/IHP/DataSync/ControllerImpl.hs index 17fd66217..37577d9e2 100644 --- a/IHP/DataSync/ControllerImpl.hs +++ b/IHP/DataSync/ControllerImpl.hs @@ -187,6 +187,49 @@ buildMessageHandler ensureRLSEnabled installTableChangeTriggers sendJSON handleC MVar.takeMVar close + handleMessage CreateCountSubscription { query, requestId } = do + ensureBelowSubscriptionsLimit + + tableNameRLS <- ensureRLSEnabled query.table + + subscriptionId <- UUID.nextRandom + + -- Allocate the close handle as early as possible + -- to make DeleteDataSubscription calls succeed even when the CountSubscription is + -- not fully set up yet + close <- MVar.newEmptyMVar + atomicModifyIORef'' ?state (\state -> state |> modify #subscriptions (HashMap.insert subscriptionId close)) + + let (theQuery, theParams) = compileQueryWithRenamer (renamer query.table) query + + let countQuery = "SELECT COUNT(*) FROM (" <> theQuery <> ") AS _inner" + + let + unpackResult :: [(Only Int)] -> Int + unpackResult [(Only value)] = value + unpackResult otherwise = error "DataSync.unpackResult: Expected INT, but got something else" + + count <- unpackResult <$> sqlQueryWithRLS countQuery theParams + countRef <- newIORef count + + installTableChangeTriggers tableNameRLS + + let + callback :: ChangeNotifications.ChangeNotification -> IO () + callback _ = do + newCount <- unpackResult <$> sqlQueryWithRLS countQuery theParams + lastCount <- readIORef countRef + + when (newCount /= count) (sendJSON DidChangeCount { subscriptionId, count = newCount }) + + let subscribe = PGListener.subscribeJSON (ChangeNotifications.channelName tableNameRLS) callback pgListener + let unsubscribe subscription = PGListener.unsubscribe subscription pgListener + + Exception.bracket subscribe unsubscribe \channelSubscription -> do + sendJSON DidCreateCountSubscription { subscriptionId, requestId, count } + + MVar.takeMVar close + handleMessage DeleteDataSubscription { requestId, subscriptionId } = do DataSyncReady { subscriptions } <- getState case HashMap.lookup subscriptionId subscriptions of diff --git a/IHP/DataSync/REST/Controller.hs b/IHP/DataSync/REST/Controller.hs index 86b052fe1..7eb12b1b9 100644 --- a/IHP/DataSync/REST/Controller.hs +++ b/IHP/DataSync/REST/Controller.hs @@ -203,7 +203,7 @@ aesonValueToPostgresValue (Number value) = case Scientific.floatingOrInteger val Left (floating :: Double) -> PG.toField floating Right (integer :: Integer) -> PG.toField integer aesonValueToPostgresValue Data.Aeson.Null = PG.toField PG.Null -aesonValueToPostgresValue (Data.Aeson.Array values) = PG.toField (PG.PGArray (Vector.toList values)) +aesonValueToPostgresValue (Data.Aeson.Array values) = PG.toField (PG.PGArray (map aesonValueToPostgresValue (Vector.toList values))) aesonValueToPostgresValue object@(Object values) = let tryDecodeAsPoint :: Maybe Point diff --git a/IHP/DataSync/Types.hs b/IHP/DataSync/Types.hs index d93be685f..ed4240d7e 100644 --- a/IHP/DataSync/Types.hs +++ b/IHP/DataSync/Types.hs @@ -10,6 +10,7 @@ import Control.Concurrent.MVar as MVar data DataSyncMessage = DataSyncQuery { query :: !DynamicSQLQuery, requestId :: !Int, transactionId :: !(Maybe UUID) } | CreateDataSubscription { query :: !DynamicSQLQuery, requestId :: !Int } + | CreateCountSubscription { query :: !DynamicSQLQuery, requestId :: !Int } | DeleteDataSubscription { subscriptionId :: !UUID, requestId :: !Int } | CreateRecordMessage { table :: !Text, record :: !(HashMap Text Value), requestId :: !Int, transactionId :: !(Maybe UUID) } | CreateRecordsMessage { table :: !Text, records :: ![HashMap Text Value], requestId :: !Int, transactionId :: !(Maybe UUID) } @@ -31,10 +32,12 @@ data DataSyncResponse | DataSyncError { requestId :: !Int, errorMessage :: !Text } | FailedToDecodeMessageError { errorMessage :: !Text } | DidCreateDataSubscription { requestId :: !Int, subscriptionId :: !UUID, result :: ![[Field]] } + | DidCreateCountSubscription { requestId :: !Int, subscriptionId :: !UUID, count :: !Int } | DidDeleteDataSubscription { requestId :: !Int, subscriptionId :: !UUID } | DidInsert { subscriptionId :: !UUID, record :: ![Field] } | DidUpdate { subscriptionId :: !UUID, id :: UUID, changeSet :: !Value } | DidDelete { subscriptionId :: !UUID, id :: !UUID } + | DidChangeCount { subscriptionId :: !UUID, count :: !Int } | DidCreateRecord { requestId :: !Int, record :: ![Field] } -- ^ Response to 'CreateRecordMessage' | DidCreateRecords { requestId :: !Int, records :: ![[Field]] } -- ^ Response to 'CreateRecordsMessage' | DidUpdateRecord { requestId :: !Int, record :: ![Field] } -- ^ Response to 'UpdateRecordMessage' diff --git a/IHP/FileStorage/ControllerFunctions.hs b/IHP/FileStorage/ControllerFunctions.hs index 6eb4cdd56..28a8daed1 100644 --- a/IHP/FileStorage/ControllerFunctions.hs +++ b/IHP/FileStorage/ControllerFunctions.hs @@ -40,7 +40,7 @@ import qualified System.Directory as Directory import qualified Control.Exception as Exception import qualified Network.Wreq as Wreq import Control.Lens hiding ((|>), set) -import IHP.FileStorage.MimeTypes +import qualified Network.Mime as Mime -- | Uploads a file to a directory in the storage -- @@ -179,7 +179,7 @@ storeFileFromUrl url options = do -- storeFileFromPath :: (?context :: context, ConfigProvider context) => Text -> StoreFileOptions -> IO StoredFile storeFileFromPath path options = do - let fileContentType = path |> guessMimeType |> cs + let fileContentType = Mime.defaultMimeLookup (cs path) fileContent <- LBS.readFile (cs path) let file = Wai.FileInfo diff --git a/IHP/FileStorage/MimeTypes.hs b/IHP/FileStorage/MimeTypes.hs deleted file mode 100644 index 9a4c3d117..000000000 --- a/IHP/FileStorage/MimeTypes.hs +++ /dev/null @@ -1,1030 +0,0 @@ -{-| -Module: IHP.FileStorage.MimeTypes -Description: Mime Type helpers -Copyright: (c) digitally induced GmbH, 2021 --} -module IHP.FileStorage.MimeTypes -( guessMimeType -, extensionToMimeTypeMapping -) where - -import IHP.Prelude -import qualified Data.Text as Text - --- | Returns the mime type based on a file extension --- --- >>> guessMimeType "test.jpg" --- "image/jpeg" --- --- Returns @application/octet-stream@ if no extension is given or it's unknown: --- --- >>> guessMimeType "unknown" --- "application/octet-stream" --- -guessMimeType :: Text -> Text -guessMimeType file = - file - |> Text.breakOnEnd "." - |> \case - (_, "") -> defaultMimeType - (_, extension) -> guessMimeTypeByExtension extension - where - defaultMimeType = "application/octet-stream" - guessMimeTypeByExtension extension = - extensionToMimeTypeMapping - |> lookup extension - |> fromMaybe defaultMimeType - --- | List of mime types --- --- Generated using: --- --- >>> wget -qO- http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types | egrep -v ^# | awk '{ for (i=2; i<=NF; i++) {print "(\""$i"\", \""$1"\")"}}' | sort -extensionToMimeTypeMapping = - [ ("123", "application/vnd.lotus-1-2-3") - , ("3dml", "text/vnd.in3d.3dml") - , ("3ds", "image/x-3ds") - , ("3g2", "video/3gpp2") - , ("3gp", "video/3gpp") - , ("7z", "application/x-7z-compressed") - , ("aab", "application/x-authorware-bin") - , ("aac", "audio/x-aac") - , ("aam", "application/x-authorware-map") - , ("aas", "application/x-authorware-seg") - , ("abw", "application/x-abiword") - , ("ac", "application/pkix-attr-cert") - , ("acc", "application/vnd.americandynamics.acc") - , ("ace", "application/x-ace-compressed") - , ("acu", "application/vnd.acucobol") - , ("acutc", "application/vnd.acucorp") - , ("adp", "audio/adpcm") - , ("aep", "application/vnd.audiograph") - , ("afm", "application/x-font-type1") - , ("afp", "application/vnd.ibm.modcap") - , ("ahead", "application/vnd.ahead.space") - , ("ai", "application/postscript") - , ("aif", "audio/x-aiff") - , ("aifc", "audio/x-aiff") - , ("aiff", "audio/x-aiff") - , ("air", "application/vnd.adobe.air-application-installer-package+zip") - , ("ait", "application/vnd.dvb.ait") - , ("ami", "application/vnd.amiga.ami") - , ("apk", "application/vnd.android.package-archive") - , ("appcache", "text/cache-manifest") - , ("application", "application/x-ms-application") - , ("apr", "application/vnd.lotus-approach") - , ("arc", "application/x-freearc") - , ("asc", "application/pgp-signature") - , ("asf", "video/x-ms-asf") - , ("asm", "text/x-asm") - , ("aso", "application/vnd.accpac.simply.aso") - , ("asx", "video/x-ms-asf") - , ("atc", "application/vnd.acucorp") - , ("atom", "application/atom+xml") - , ("atomcat", "application/atomcat+xml") - , ("atomsvc", "application/atomsvc+xml") - , ("atx", "application/vnd.antix.game-component") - , ("au", "audio/basic") - , ("avi", "video/x-msvideo") - , ("aw", "application/applixware") - , ("azf", "application/vnd.airzip.filesecure.azf") - , ("azs", "application/vnd.airzip.filesecure.azs") - , ("azw", "application/vnd.amazon.ebook") - , ("bat", "application/x-msdownload") - , ("bcpio", "application/x-bcpio") - , ("bdf", "application/x-font-bdf") - , ("bdm", "application/vnd.syncml.dm+wbxml") - , ("bed", "application/vnd.realvnc.bed") - , ("bh2", "application/vnd.fujitsu.oasysprs") - , ("bin", "application/octet-stream") - , ("blb", "application/x-blorb") - , ("blorb", "application/x-blorb") - , ("bmi", "application/vnd.bmi") - , ("bmp", "image/bmp") - , ("book", "application/vnd.framemaker") - , ("box", "application/vnd.previewsystems.box") - , ("boz", "application/x-bzip2") - , ("bpk", "application/octet-stream") - , ("btif", "image/prs.btif") - , ("bz", "application/x-bzip") - , ("bz2", "application/x-bzip2") - , ("c", "text/x-c") - , ("c11amc", "application/vnd.cluetrust.cartomobile-config") - , ("c11amz", "application/vnd.cluetrust.cartomobile-config-pkg") - , ("c4d", "application/vnd.clonk.c4group") - , ("c4f", "application/vnd.clonk.c4group") - , ("c4g", "application/vnd.clonk.c4group") - , ("c4p", "application/vnd.clonk.c4group") - , ("c4u", "application/vnd.clonk.c4group") - , ("cab", "application/vnd.ms-cab-compressed") - , ("caf", "audio/x-caf") - , ("cap", "application/vnd.tcpdump.pcap") - , ("car", "application/vnd.curl.car") - , ("cat", "application/vnd.ms-pki.seccat") - , ("cb7", "application/x-cbr") - , ("cba", "application/x-cbr") - , ("cbr", "application/x-cbr") - , ("cbt", "application/x-cbr") - , ("cbz", "application/x-cbr") - , ("cc", "text/x-c") - , ("cct", "application/x-director") - , ("ccxml", "application/ccxml+xml") - , ("cdbcmsg", "application/vnd.contact.cmsg") - , ("cdf", "application/x-netcdf") - , ("cdkey", "application/vnd.mediastation.cdkey") - , ("cdmia", "application/cdmi-capability") - , ("cdmic", "application/cdmi-container") - , ("cdmid", "application/cdmi-domain") - , ("cdmio", "application/cdmi-object") - , ("cdmiq", "application/cdmi-queue") - , ("cdx", "chemical/x-cdx") - , ("cdxml", "application/vnd.chemdraw+xml") - , ("cdy", "application/vnd.cinderella") - , ("cer", "application/pkix-cert") - , ("cfs", "application/x-cfs-compressed") - , ("cgm", "image/cgm") - , ("chat", "application/x-chat") - , ("chm", "application/vnd.ms-htmlhelp") - , ("chrt", "application/vnd.kde.kchart") - , ("cif", "chemical/x-cif") - , ("cii", "application/vnd.anser-web-certificate-issue-initiation") - , ("cil", "application/vnd.ms-artgalry") - , ("cla", "application/vnd.claymore") - , ("class", "application/java-vm") - , ("clkk", "application/vnd.crick.clicker.keyboard") - , ("clkp", "application/vnd.crick.clicker.palette") - , ("clkt", "application/vnd.crick.clicker.template") - , ("clkw", "application/vnd.crick.clicker.wordbank") - , ("clkx", "application/vnd.crick.clicker") - , ("clp", "application/x-msclip") - , ("cmc", "application/vnd.cosmocaller") - , ("cmdf", "chemical/x-cmdf") - , ("cml", "chemical/x-cml") - , ("cmp", "application/vnd.yellowriver-custom-menu") - , ("cmx", "image/x-cmx") - , ("cod", "application/vnd.rim.cod") - , ("com", "application/x-msdownload") - , ("conf", "text/plain") - , ("cpio", "application/x-cpio") - , ("cpp", "text/x-c") - , ("cpt", "application/mac-compactpro") - , ("crd", "application/x-mscardfile") - , ("crl", "application/pkix-crl") - , ("crt", "application/x-x509-ca-cert") - , ("cryptonote", "application/vnd.rig.cryptonote") - , ("csh", "application/x-csh") - , ("csml", "chemical/x-csml") - , ("csp", "application/vnd.commonspace") - , ("css", "text/css") - , ("cst", "application/x-director") - , ("csv", "text/csv") - , ("cu", "application/cu-seeme") - , ("curl", "text/vnd.curl") - , ("cww", "application/prs.cww") - , ("cxt", "application/x-director") - , ("cxx", "text/x-c") - , ("dae", "model/vnd.collada+xml") - , ("daf", "application/vnd.mobius.daf") - , ("dart", "application/vnd.dart") - , ("dataless", "application/vnd.fdsn.seed") - , ("davmount", "application/davmount+xml") - , ("dbk", "application/docbook+xml") - , ("dcr", "application/x-director") - , ("dcurl", "text/vnd.curl.dcurl") - , ("dd2", "application/vnd.oma.dd2+xml") - , ("ddd", "application/vnd.fujixerox.ddd") - , ("deb", "application/x-debian-package") - , ("def", "text/plain") - , ("deploy", "application/octet-stream") - , ("der", "application/x-x509-ca-cert") - , ("dfac", "application/vnd.dreamfactory") - , ("dgc", "application/x-dgc-compressed") - , ("dic", "text/x-c") - , ("dir", "application/x-director") - , ("dis", "application/vnd.mobius.dis") - , ("dist", "application/octet-stream") - , ("distz", "application/octet-stream") - , ("djv", "image/vnd.djvu") - , ("djvu", "image/vnd.djvu") - , ("dll", "application/x-msdownload") - , ("dmg", "application/x-apple-diskimage") - , ("dmp", "application/vnd.tcpdump.pcap") - , ("dms", "application/octet-stream") - , ("dna", "application/vnd.dna") - , ("doc", "application/msword") - , ("docm", "application/vnd.ms-word.document.macroenabled.12") - , ("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document") - , ("dot", "application/msword") - , ("dotm", "application/vnd.ms-word.template.macroenabled.12") - , ("dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template") - , ("dp", "application/vnd.osgi.dp") - , ("dpg", "application/vnd.dpgraph") - , ("dra", "audio/vnd.dra") - , ("dsc", "text/prs.lines.tag") - , ("dssc", "application/dssc+der") - , ("dtb", "application/x-dtbook+xml") - , ("dtd", "application/xml-dtd") - , ("dts", "audio/vnd.dts") - , ("dtshd", "audio/vnd.dts.hd") - , ("dump", "application/octet-stream") - , ("dvb", "video/vnd.dvb.file") - , ("dvi", "application/x-dvi") - , ("dwf", "model/vnd.dwf") - , ("dwg", "image/vnd.dwg") - , ("dxf", "image/vnd.dxf") - , ("dxp", "application/vnd.spotfire.dxp") - , ("dxr", "application/x-director") - , ("ecelp4800", "audio/vnd.nuera.ecelp4800") - , ("ecelp7470", "audio/vnd.nuera.ecelp7470") - , ("ecelp9600", "audio/vnd.nuera.ecelp9600") - , ("ecma", "application/ecmascript") - , ("edm", "application/vnd.novadigm.edm") - , ("edx", "application/vnd.novadigm.edx") - , ("efif", "application/vnd.picsel") - , ("ei6", "application/vnd.pg.osasli") - , ("elc", "application/octet-stream") - , ("emf", "application/x-msmetafile") - , ("eml", "message/rfc822") - , ("emma", "application/emma+xml") - , ("emz", "application/x-msmetafile") - , ("eol", "audio/vnd.digital-winds") - , ("eot", "application/vnd.ms-fontobject") - , ("eps", "application/postscript") - , ("epub", "application/epub+zip") - , ("es3", "application/vnd.eszigno3+xml") - , ("esa", "application/vnd.osgi.subsystem") - , ("esf", "application/vnd.epson.esf") - , ("et3", "application/vnd.eszigno3+xml") - , ("etx", "text/x-setext") - , ("eva", "application/x-eva") - , ("evy", "application/x-envoy") - , ("exe", "application/x-msdownload") - , ("exi", "application/exi") - , ("ext", "application/vnd.novadigm.ext") - , ("ez", "application/andrew-inset") - , ("ez2", "application/vnd.ezpix-album") - , ("ez3", "application/vnd.ezpix-package") - , ("f", "text/x-fortran") - , ("f4v", "video/x-f4v") - , ("f77", "text/x-fortran") - , ("f90", "text/x-fortran") - , ("fbs", "image/vnd.fastbidsheet") - , ("fcdt", "application/vnd.adobe.formscentral.fcdt") - , ("fcs", "application/vnd.isac.fcs") - , ("fdf", "application/vnd.fdf") - , ("fe_launch", "application/vnd.denovo.fcselayout-link") - , ("fg5", "application/vnd.fujitsu.oasysgp") - , ("fgd", "application/x-director") - , ("fh", "image/x-freehand") - , ("fh4", "image/x-freehand") - , ("fh5", "image/x-freehand") - , ("fh7", "image/x-freehand") - , ("fhc", "image/x-freehand") - , ("fig", "application/x-xfig") - , ("flac", "audio/x-flac") - , ("fli", "video/x-fli") - , ("flo", "application/vnd.micrografx.flo") - , ("flv", "video/x-flv") - , ("flw", "application/vnd.kde.kivio") - , ("flx", "text/vnd.fmi.flexstor") - , ("fly", "text/vnd.fly") - , ("fm", "application/vnd.framemaker") - , ("fnc", "application/vnd.frogans.fnc") - , ("for", "text/x-fortran") - , ("fpx", "image/vnd.fpx") - , ("frame", "application/vnd.framemaker") - , ("fsc", "application/vnd.fsc.weblaunch") - , ("fst", "image/vnd.fst") - , ("ftc", "application/vnd.fluxtime.clip") - , ("fti", "application/vnd.anser-web-funds-transfer-initiation") - , ("fvt", "video/vnd.fvt") - , ("fxp", "application/vnd.adobe.fxp") - , ("fxpl", "application/vnd.adobe.fxp") - , ("fzs", "application/vnd.fuzzysheet") - , ("g2w", "application/vnd.geoplan") - , ("g3", "image/g3fax") - , ("g3w", "application/vnd.geospace") - , ("gac", "application/vnd.groove-account") - , ("gam", "application/x-tads") - , ("gbr", "application/rpki-ghostbusters") - , ("gca", "application/x-gca-compressed") - , ("gdl", "model/vnd.gdl") - , ("geo", "application/vnd.dynageo") - , ("gex", "application/vnd.geometry-explorer") - , ("ggb", "application/vnd.geogebra.file") - , ("ggt", "application/vnd.geogebra.tool") - , ("ghf", "application/vnd.groove-help") - , ("gif", "image/gif") - , ("gim", "application/vnd.groove-identity-message") - , ("gml", "application/gml+xml") - , ("gmx", "application/vnd.gmx") - , ("gnumeric", "application/x-gnumeric") - , ("gph", "application/vnd.flographit") - , ("gpx", "application/gpx+xml") - , ("gqf", "application/vnd.grafeq") - , ("gqs", "application/vnd.grafeq") - , ("gram", "application/srgs") - , ("gramps", "application/x-gramps-xml") - , ("gre", "application/vnd.geometry-explorer") - , ("grv", "application/vnd.groove-injector") - , ("grxml", "application/srgs+xml") - , ("gsf", "application/x-font-ghostscript") - , ("gtar", "application/x-gtar") - , ("gtm", "application/vnd.groove-tool-message") - , ("gtw", "model/vnd.gtw") - , ("gv", "text/vnd.graphviz") - , ("gxf", "application/gxf") - , ("gxt", "application/vnd.geonext") - , ("h", "text/x-c") - , ("h261", "video/h261") - , ("h263", "video/h263") - , ("h264", "video/h264") - , ("hal", "application/vnd.hal+xml") - , ("hbci", "application/vnd.hbci") - , ("hdf", "application/x-hdf") - , ("hh", "text/x-c") - , ("hlp", "application/winhlp") - , ("hpgl", "application/vnd.hp-hpgl") - , ("hpid", "application/vnd.hp-hpid") - , ("hps", "application/vnd.hp-hps") - , ("hqx", "application/mac-binhex40") - , ("htke", "application/vnd.kenameaapp") - , ("htm", "text/html") - , ("html", "text/html") - , ("hvd", "application/vnd.yamaha.hv-dic") - , ("hvp", "application/vnd.yamaha.hv-voice") - , ("hvs", "application/vnd.yamaha.hv-script") - , ("i2g", "application/vnd.intergeo") - , ("icc", "application/vnd.iccprofile") - , ("ice", "x-conference/x-cooltalk") - , ("icm", "application/vnd.iccprofile") - , ("ico", "image/x-icon") - , ("ics", "text/calendar") - , ("ief", "image/ief") - , ("ifb", "text/calendar") - , ("ifm", "application/vnd.shana.informed.formdata") - , ("iges", "model/iges") - , ("igl", "application/vnd.igloader") - , ("igm", "application/vnd.insors.igm") - , ("igs", "model/iges") - , ("igx", "application/vnd.micrografx.igx") - , ("iif", "application/vnd.shana.informed.interchange") - , ("imp", "application/vnd.accpac.simply.imp") - , ("ims", "application/vnd.ms-ims") - , ("in", "text/plain") - , ("ink", "application/inkml+xml") - , ("inkml", "application/inkml+xml") - , ("install", "application/x-install-instructions") - , ("iota", "application/vnd.astraea-software.iota") - , ("ipfix", "application/ipfix") - , ("ipk", "application/vnd.shana.informed.package") - , ("irm", "application/vnd.ibm.rights-management") - , ("irp", "application/vnd.irepository.package+xml") - , ("iso", "application/x-iso9660-image") - , ("itp", "application/vnd.shana.informed.formtemplate") - , ("ivp", "application/vnd.immervision-ivp") - , ("ivu", "application/vnd.immervision-ivu") - , ("jad", "text/vnd.sun.j2me.app-descriptor") - , ("jam", "application/vnd.jam") - , ("jar", "application/java-archive") - , ("java", "text/x-java-source") - , ("jisp", "application/vnd.jisp") - , ("jlt", "application/vnd.hp-jlyt") - , ("jnlp", "application/x-java-jnlp-file") - , ("joda", "application/vnd.joost.joda-archive") - , ("jpe", "image/jpeg") - , ("jpeg", "image/jpeg") - , ("jpg", "image/jpeg") - , ("jpgm", "video/jpm") - , ("jpgv", "video/jpeg") - , ("jpm", "video/jpm") - , ("js", "application/javascript") - , ("json", "application/json") - , ("jsonml", "application/jsonml+json") - , ("kar", "audio/midi") - , ("karbon", "application/vnd.kde.karbon") - , ("kfo", "application/vnd.kde.kformula") - , ("kia", "application/vnd.kidspiration") - , ("kml", "application/vnd.google-earth.kml+xml") - , ("kmz", "application/vnd.google-earth.kmz") - , ("kne", "application/vnd.kinar") - , ("knp", "application/vnd.kinar") - , ("kon", "application/vnd.kde.kontour") - , ("kpr", "application/vnd.kde.kpresenter") - , ("kpt", "application/vnd.kde.kpresenter") - , ("kpxx", "application/vnd.ds-keypoint") - , ("ksp", "application/vnd.kde.kspread") - , ("ktr", "application/vnd.kahootz") - , ("ktx", "image/ktx") - , ("ktz", "application/vnd.kahootz") - , ("kwd", "application/vnd.kde.kword") - , ("kwt", "application/vnd.kde.kword") - , ("lasxml", "application/vnd.las.las+xml") - , ("latex", "application/x-latex") - , ("lbd", "application/vnd.llamagraphics.life-balance.desktop") - , ("lbe", "application/vnd.llamagraphics.life-balance.exchange+xml") - , ("les", "application/vnd.hhe.lesson-player") - , ("lha", "application/x-lzh-compressed") - , ("link66", "application/vnd.route66.link66+xml") - , ("list", "text/plain") - , ("list3820", "application/vnd.ibm.modcap") - , ("listafp", "application/vnd.ibm.modcap") - , ("lnk", "application/x-ms-shortcut") - , ("log", "text/plain") - , ("lostxml", "application/lost+xml") - , ("lrf", "application/octet-stream") - , ("lrm", "application/vnd.ms-lrm") - , ("ltf", "application/vnd.frogans.ltf") - , ("lvp", "audio/vnd.lucent.voice") - , ("lwp", "application/vnd.lotus-wordpro") - , ("lzh", "application/x-lzh-compressed") - , ("m13", "application/x-msmediaview") - , ("m14", "application/x-msmediaview") - , ("m1v", "video/mpeg") - , ("m21", "application/mp21") - , ("m2a", "audio/mpeg") - , ("m2v", "video/mpeg") - , ("m3a", "audio/mpeg") - , ("m3u", "audio/x-mpegurl") - , ("m3u8", "application/vnd.apple.mpegurl") - , ("m4a", "audio/mp4") - , ("m4u", "video/vnd.mpegurl") - , ("m4v", "video/x-m4v") - , ("ma", "application/mathematica") - , ("mads", "application/mads+xml") - , ("mag", "application/vnd.ecowin.chart") - , ("maker", "application/vnd.framemaker") - , ("man", "text/troff") - , ("mar", "application/octet-stream") - , ("mathml", "application/mathml+xml") - , ("mb", "application/mathematica") - , ("mbk", "application/vnd.mobius.mbk") - , ("mbox", "application/mbox") - , ("mc1", "application/vnd.medcalcdata") - , ("mcd", "application/vnd.mcd") - , ("mcurl", "text/vnd.curl.mcurl") - , ("mdb", "application/x-msaccess") - , ("mdi", "image/vnd.ms-modi") - , ("me", "text/troff") - , ("mesh", "model/mesh") - , ("meta4", "application/metalink4+xml") - , ("metalink", "application/metalink+xml") - , ("mets", "application/mets+xml") - , ("mfm", "application/vnd.mfmp") - , ("mft", "application/rpki-manifest") - , ("mgp", "application/vnd.osgeo.mapguide.package") - , ("mgz", "application/vnd.proteus.magazine") - , ("mid", "audio/midi") - , ("midi", "audio/midi") - , ("mie", "application/x-mie") - , ("mif", "application/vnd.mif") - , ("mime", "message/rfc822") - , ("mj2", "video/mj2") - , ("mjp2", "video/mj2") - , ("mk3d", "video/x-matroska") - , ("mka", "audio/x-matroska") - , ("mks", "video/x-matroska") - , ("mkv", "video/x-matroska") - , ("mlp", "application/vnd.dolby.mlp") - , ("mmd", "application/vnd.chipnuts.karaoke-mmd") - , ("mmf", "application/vnd.smaf") - , ("mmr", "image/vnd.fujixerox.edmics-mmr") - , ("mng", "video/x-mng") - , ("mny", "application/x-msmoney") - , ("mobi", "application/x-mobipocket-ebook") - , ("mods", "application/mods+xml") - , ("mov", "video/quicktime") - , ("movie", "video/x-sgi-movie") - , ("mp2", "audio/mpeg") - , ("mp21", "application/mp21") - , ("mp2a", "audio/mpeg") - , ("mp3", "audio/mpeg") - , ("mp4", "video/mp4") - , ("mp4a", "audio/mp4") - , ("mp4s", "application/mp4") - , ("mp4v", "video/mp4") - , ("mpc", "application/vnd.mophun.certificate") - , ("mpe", "video/mpeg") - , ("mpeg", "video/mpeg") - , ("mpg", "video/mpeg") - , ("mpg4", "video/mp4") - , ("mpga", "audio/mpeg") - , ("mpkg", "application/vnd.apple.installer+xml") - , ("mpm", "application/vnd.blueice.multipass") - , ("mpn", "application/vnd.mophun.application") - , ("mpp", "application/vnd.ms-project") - , ("mpt", "application/vnd.ms-project") - , ("mpy", "application/vnd.ibm.minipay") - , ("mqy", "application/vnd.mobius.mqy") - , ("mrc", "application/marc") - , ("mrcx", "application/marcxml+xml") - , ("ms", "text/troff") - , ("mscml", "application/mediaservercontrol+xml") - , ("mseed", "application/vnd.fdsn.mseed") - , ("mseq", "application/vnd.mseq") - , ("msf", "application/vnd.epson.msf") - , ("msh", "model/mesh") - , ("msi", "application/x-msdownload") - , ("msl", "application/vnd.mobius.msl") - , ("msty", "application/vnd.muvee.style") - , ("mts", "model/vnd.mts") - , ("mus", "application/vnd.musician") - , ("musicxml", "application/vnd.recordare.musicxml+xml") - , ("mvb", "application/x-msmediaview") - , ("mwf", "application/vnd.mfer") - , ("mxf", "application/mxf") - , ("mxl", "application/vnd.recordare.musicxml") - , ("mxml", "application/xv+xml") - , ("mxs", "application/vnd.triscape.mxs") - , ("mxu", "video/vnd.mpegurl") - , ("n-gage", "application/vnd.nokia.n-gage.symbian.install") - , ("n3", "text/n3") - , ("nb", "application/mathematica") - , ("nbp", "application/vnd.wolfram.player") - , ("nc", "application/x-netcdf") - , ("ncx", "application/x-dtbncx+xml") - , ("nfo", "text/x-nfo") - , ("ngdat", "application/vnd.nokia.n-gage.data") - , ("nitf", "application/vnd.nitf") - , ("nlu", "application/vnd.neurolanguage.nlu") - , ("nml", "application/vnd.enliven") - , ("nnd", "application/vnd.noblenet-directory") - , ("nns", "application/vnd.noblenet-sealer") - , ("nnw", "application/vnd.noblenet-web") - , ("npx", "image/vnd.net-fpx") - , ("nsc", "application/x-conference") - , ("nsf", "application/vnd.lotus-notes") - , ("ntf", "application/vnd.nitf") - , ("nzb", "application/x-nzb") - , ("oa2", "application/vnd.fujitsu.oasys2") - , ("oa3", "application/vnd.fujitsu.oasys3") - , ("oas", "application/vnd.fujitsu.oasys") - , ("obd", "application/x-msbinder") - , ("obj", "application/x-tgif") - , ("oda", "application/oda") - , ("odb", "application/vnd.oasis.opendocument.database") - , ("odc", "application/vnd.oasis.opendocument.chart") - , ("odf", "application/vnd.oasis.opendocument.formula") - , ("odft", "application/vnd.oasis.opendocument.formula-template") - , ("odg", "application/vnd.oasis.opendocument.graphics") - , ("odi", "application/vnd.oasis.opendocument.image") - , ("odm", "application/vnd.oasis.opendocument.text-master") - , ("odp", "application/vnd.oasis.opendocument.presentation") - , ("ods", "application/vnd.oasis.opendocument.spreadsheet") - , ("odt", "application/vnd.oasis.opendocument.text") - , ("oga", "audio/ogg") - , ("ogg", "audio/ogg") - , ("ogv", "video/ogg") - , ("ogx", "application/ogg") - , ("omdoc", "application/omdoc+xml") - , ("onepkg", "application/onenote") - , ("onetmp", "application/onenote") - , ("onetoc", "application/onenote") - , ("onetoc2", "application/onenote") - , ("opf", "application/oebps-package+xml") - , ("opml", "text/x-opml") - , ("oprc", "application/vnd.palm") - , ("opus", "audio/ogg") - , ("org", "application/vnd.lotus-organizer") - , ("osf", "application/vnd.yamaha.openscoreformat") - , ("osfpvg", "application/vnd.yamaha.openscoreformat.osfpvg+xml") - , ("otc", "application/vnd.oasis.opendocument.chart-template") - , ("otf", "font/otf") - , ("otg", "application/vnd.oasis.opendocument.graphics-template") - , ("oth", "application/vnd.oasis.opendocument.text-web") - , ("oti", "application/vnd.oasis.opendocument.image-template") - , ("otp", "application/vnd.oasis.opendocument.presentation-template") - , ("ots", "application/vnd.oasis.opendocument.spreadsheet-template") - , ("ott", "application/vnd.oasis.opendocument.text-template") - , ("oxps", "application/oxps") - , ("oxt", "application/vnd.openofficeorg.extension") - , ("p", "text/x-pascal") - , ("p10", "application/pkcs10") - , ("p12", "application/x-pkcs12") - , ("p7b", "application/x-pkcs7-certificates") - , ("p7c", "application/pkcs7-mime") - , ("p7m", "application/pkcs7-mime") - , ("p7r", "application/x-pkcs7-certreqresp") - , ("p7s", "application/pkcs7-signature") - , ("p8", "application/pkcs8") - , ("pas", "text/x-pascal") - , ("paw", "application/vnd.pawaafile") - , ("pbd", "application/vnd.powerbuilder6") - , ("pbm", "image/x-portable-bitmap") - , ("pcap", "application/vnd.tcpdump.pcap") - , ("pcf", "application/x-font-pcf") - , ("pcl", "application/vnd.hp-pcl") - , ("pclxl", "application/vnd.hp-pclxl") - , ("pct", "image/x-pict") - , ("pcurl", "application/vnd.curl.pcurl") - , ("pcx", "image/x-pcx") - , ("pdb", "application/vnd.palm") - , ("pdf", "application/pdf") - , ("pfa", "application/x-font-type1") - , ("pfb", "application/x-font-type1") - , ("pfm", "application/x-font-type1") - , ("pfr", "application/font-tdpfr") - , ("pfx", "application/x-pkcs12") - , ("pgm", "image/x-portable-graymap") - , ("pgn", "application/x-chess-pgn") - , ("pgp", "application/pgp-encrypted") - , ("pic", "image/x-pict") - , ("pkg", "application/octet-stream") - , ("pki", "application/pkixcmp") - , ("pkipath", "application/pkix-pkipath") - , ("plb", "application/vnd.3gpp.pic-bw-large") - , ("plc", "application/vnd.mobius.plc") - , ("plf", "application/vnd.pocketlearn") - , ("pls", "application/pls+xml") - , ("pml", "application/vnd.ctc-posml") - , ("png", "image/png") - , ("pnm", "image/x-portable-anymap") - , ("portpkg", "application/vnd.macports.portpkg") - , ("pot", "application/vnd.ms-powerpoint") - , ("potm", "application/vnd.ms-powerpoint.template.macroenabled.12") - , ("potx", "application/vnd.openxmlformats-officedocument.presentationml.template") - , ("ppam", "application/vnd.ms-powerpoint.addin.macroenabled.12") - , ("ppd", "application/vnd.cups-ppd") - , ("ppm", "image/x-portable-pixmap") - , ("pps", "application/vnd.ms-powerpoint") - , ("ppsm", "application/vnd.ms-powerpoint.slideshow.macroenabled.12") - , ("ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow") - , ("ppt", "application/vnd.ms-powerpoint") - , ("pptm", "application/vnd.ms-powerpoint.presentation.macroenabled.12") - , ("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation") - , ("pqa", "application/vnd.palm") - , ("prc", "application/x-mobipocket-ebook") - , ("pre", "application/vnd.lotus-freelance") - , ("prf", "application/pics-rules") - , ("ps", "application/postscript") - , ("psb", "application/vnd.3gpp.pic-bw-small") - , ("psd", "image/vnd.adobe.photoshop") - , ("psf", "application/x-font-linux-psf") - , ("pskcxml", "application/pskc+xml") - , ("ptid", "application/vnd.pvi.ptid1") - , ("pub", "application/x-mspublisher") - , ("pvb", "application/vnd.3gpp.pic-bw-var") - , ("pwn", "application/vnd.3m.post-it-notes") - , ("pya", "audio/vnd.ms-playready.media.pya") - , ("pyv", "video/vnd.ms-playready.media.pyv") - , ("qam", "application/vnd.epson.quickanime") - , ("qbo", "application/vnd.intu.qbo") - , ("qfx", "application/vnd.intu.qfx") - , ("qps", "application/vnd.publishare-delta-tree") - , ("qt", "video/quicktime") - , ("qwd", "application/vnd.quark.quarkxpress") - , ("qwt", "application/vnd.quark.quarkxpress") - , ("qxb", "application/vnd.quark.quarkxpress") - , ("qxd", "application/vnd.quark.quarkxpress") - , ("qxl", "application/vnd.quark.quarkxpress") - , ("qxt", "application/vnd.quark.quarkxpress") - , ("ra", "audio/x-pn-realaudio") - , ("ram", "audio/x-pn-realaudio") - , ("rar", "application/x-rar-compressed") - , ("ras", "image/x-cmu-raster") - , ("rcprofile", "application/vnd.ipunplugged.rcprofile") - , ("rdf", "application/rdf+xml") - , ("rdz", "application/vnd.data-vision.rdz") - , ("rep", "application/vnd.businessobjects") - , ("res", "application/x-dtbresource+xml") - , ("rgb", "image/x-rgb") - , ("rif", "application/reginfo+xml") - , ("rip", "audio/vnd.rip") - , ("ris", "application/x-research-info-systems") - , ("rl", "application/resource-lists+xml") - , ("rlc", "image/vnd.fujixerox.edmics-rlc") - , ("rld", "application/resource-lists-diff+xml") - , ("rm", "application/vnd.rn-realmedia") - , ("rmi", "audio/midi") - , ("rmp", "audio/x-pn-realaudio-plugin") - , ("rms", "application/vnd.jcp.javame.midlet-rms") - , ("rmvb", "application/vnd.rn-realmedia-vbr") - , ("rnc", "application/relax-ng-compact-syntax") - , ("roa", "application/rpki-roa") - , ("roff", "text/troff") - , ("rp9", "application/vnd.cloanto.rp9") - , ("rpss", "application/vnd.nokia.radio-presets") - , ("rpst", "application/vnd.nokia.radio-preset") - , ("rq", "application/sparql-query") - , ("rs", "application/rls-services+xml") - , ("rsd", "application/rsd+xml") - , ("rss", "application/rss+xml") - , ("rtf", "application/rtf") - , ("rtx", "text/richtext") - , ("s", "text/x-asm") - , ("s3m", "audio/s3m") - , ("saf", "application/vnd.yamaha.smaf-audio") - , ("sbml", "application/sbml+xml") - , ("sc", "application/vnd.ibm.secure-container") - , ("scd", "application/x-msschedule") - , ("scm", "application/vnd.lotus-screencam") - , ("scq", "application/scvp-cv-request") - , ("scs", "application/scvp-cv-response") - , ("scurl", "text/vnd.curl.scurl") - , ("sda", "application/vnd.stardivision.draw") - , ("sdc", "application/vnd.stardivision.calc") - , ("sdd", "application/vnd.stardivision.impress") - , ("sdkd", "application/vnd.solent.sdkm+xml") - , ("sdkm", "application/vnd.solent.sdkm+xml") - , ("sdp", "application/sdp") - , ("sdw", "application/vnd.stardivision.writer") - , ("see", "application/vnd.seemail") - , ("seed", "application/vnd.fdsn.seed") - , ("sema", "application/vnd.sema") - , ("semd", "application/vnd.semd") - , ("semf", "application/vnd.semf") - , ("ser", "application/java-serialized-object") - , ("setpay", "application/set-payment-initiation") - , ("setreg", "application/set-registration-initiation") - , ("sfd-hdstx", "application/vnd.hydrostatix.sof-data") - , ("sfs", "application/vnd.spotfire.sfs") - , ("sfv", "text/x-sfv") - , ("sgi", "image/sgi") - , ("sgl", "application/vnd.stardivision.writer-global") - , ("sgm", "text/sgml") - , ("sgml", "text/sgml") - , ("sh", "application/x-sh") - , ("shar", "application/x-shar") - , ("shf", "application/shf+xml") - , ("sid", "image/x-mrsid-image") - , ("sig", "application/pgp-signature") - , ("sil", "audio/silk") - , ("silo", "model/mesh") - , ("sis", "application/vnd.symbian.install") - , ("sisx", "application/vnd.symbian.install") - , ("sit", "application/x-stuffit") - , ("sitx", "application/x-stuffitx") - , ("skd", "application/vnd.koan") - , ("skm", "application/vnd.koan") - , ("skp", "application/vnd.koan") - , ("skt", "application/vnd.koan") - , ("sldm", "application/vnd.ms-powerpoint.slide.macroenabled.12") - , ("sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide") - , ("slt", "application/vnd.epson.salt") - , ("sm", "application/vnd.stepmania.stepchart") - , ("smf", "application/vnd.stardivision.math") - , ("smi", "application/smil+xml") - , ("smil", "application/smil+xml") - , ("smv", "video/x-smv") - , ("smzip", "application/vnd.stepmania.package") - , ("snd", "audio/basic") - , ("snf", "application/x-font-snf") - , ("so", "application/octet-stream") - , ("spc", "application/x-pkcs7-certificates") - , ("spf", "application/vnd.yamaha.smaf-phrase") - , ("spl", "application/x-futuresplash") - , ("spot", "text/vnd.in3d.spot") - , ("spp", "application/scvp-vp-response") - , ("spq", "application/scvp-vp-request") - , ("spx", "audio/ogg") - , ("sql", "application/x-sql") - , ("src", "application/x-wais-source") - , ("srt", "application/x-subrip") - , ("sru", "application/sru+xml") - , ("srx", "application/sparql-results+xml") - , ("ssdl", "application/ssdl+xml") - , ("sse", "application/vnd.kodak-descriptor") - , ("ssf", "application/vnd.epson.ssf") - , ("ssml", "application/ssml+xml") - , ("st", "application/vnd.sailingtracker.track") - , ("stc", "application/vnd.sun.xml.calc.template") - , ("std", "application/vnd.sun.xml.draw.template") - , ("stf", "application/vnd.wt.stf") - , ("sti", "application/vnd.sun.xml.impress.template") - , ("stk", "application/hyperstudio") - , ("stl", "application/vnd.ms-pki.stl") - , ("str", "application/vnd.pg.format") - , ("stw", "application/vnd.sun.xml.writer.template") - , ("sub", "image/vnd.dvb.subtitle") - , ("sub", "text/vnd.dvb.subtitle") - , ("sus", "application/vnd.sus-calendar") - , ("susp", "application/vnd.sus-calendar") - , ("sv4cpio", "application/x-sv4cpio") - , ("sv4crc", "application/x-sv4crc") - , ("svc", "application/vnd.dvb.service") - , ("svd", "application/vnd.svd") - , ("svg", "image/svg+xml") - , ("svgz", "image/svg+xml") - , ("swa", "application/x-director") - , ("swf", "application/x-shockwave-flash") - , ("swi", "application/vnd.aristanetworks.swi") - , ("sxc", "application/vnd.sun.xml.calc") - , ("sxd", "application/vnd.sun.xml.draw") - , ("sxg", "application/vnd.sun.xml.writer.global") - , ("sxi", "application/vnd.sun.xml.impress") - , ("sxm", "application/vnd.sun.xml.math") - , ("sxw", "application/vnd.sun.xml.writer") - , ("t", "text/troff") - , ("t3", "application/x-t3vm-image") - , ("taglet", "application/vnd.mynfc") - , ("tao", "application/vnd.tao.intent-module-archive") - , ("tar", "application/x-tar") - , ("tcap", "application/vnd.3gpp2.tcap") - , ("tcl", "application/x-tcl") - , ("teacher", "application/vnd.smart.teacher") - , ("tei", "application/tei+xml") - , ("teicorpus", "application/tei+xml") - , ("tex", "application/x-tex") - , ("texi", "application/x-texinfo") - , ("texinfo", "application/x-texinfo") - , ("text", "text/plain") - , ("tfi", "application/thraud+xml") - , ("tfm", "application/x-tex-tfm") - , ("tga", "image/x-tga") - , ("thmx", "application/vnd.ms-officetheme") - , ("tif", "image/tiff") - , ("tiff", "image/tiff") - , ("tmo", "application/vnd.tmobile-livetv") - , ("torrent", "application/x-bittorrent") - , ("tpl", "application/vnd.groove-tool-template") - , ("tpt", "application/vnd.trid.tpt") - , ("tr", "text/troff") - , ("tra", "application/vnd.trueapp") - , ("trm", "application/x-msterminal") - , ("tsd", "application/timestamped-data") - , ("tsv", "text/tab-separated-values") - , ("ttc", "font/collection") - , ("ttf", "font/ttf") - , ("ttl", "text/turtle") - , ("twd", "application/vnd.simtech-mindmapper") - , ("twds", "application/vnd.simtech-mindmapper") - , ("txd", "application/vnd.genomatix.tuxedo") - , ("txf", "application/vnd.mobius.txf") - , ("txt", "text/plain") - , ("u32", "application/x-authorware-bin") - , ("udeb", "application/x-debian-package") - , ("ufd", "application/vnd.ufdl") - , ("ufdl", "application/vnd.ufdl") - , ("ulx", "application/x-glulx") - , ("umj", "application/vnd.umajin") - , ("unityweb", "application/vnd.unity") - , ("uoml", "application/vnd.uoml+xml") - , ("uri", "text/uri-list") - , ("uris", "text/uri-list") - , ("urls", "text/uri-list") - , ("ustar", "application/x-ustar") - , ("utz", "application/vnd.uiq.theme") - , ("uu", "text/x-uuencode") - , ("uva", "audio/vnd.dece.audio") - , ("uvd", "application/vnd.dece.data") - , ("uvf", "application/vnd.dece.data") - , ("uvg", "image/vnd.dece.graphic") - , ("uvh", "video/vnd.dece.hd") - , ("uvi", "image/vnd.dece.graphic") - , ("uvm", "video/vnd.dece.mobile") - , ("uvp", "video/vnd.dece.pd") - , ("uvs", "video/vnd.dece.sd") - , ("uvt", "application/vnd.dece.ttml+xml") - , ("uvu", "video/vnd.uvvu.mp4") - , ("uvv", "video/vnd.dece.video") - , ("uvva", "audio/vnd.dece.audio") - , ("uvvd", "application/vnd.dece.data") - , ("uvvf", "application/vnd.dece.data") - , ("uvvg", "image/vnd.dece.graphic") - , ("uvvh", "video/vnd.dece.hd") - , ("uvvi", "image/vnd.dece.graphic") - , ("uvvm", "video/vnd.dece.mobile") - , ("uvvp", "video/vnd.dece.pd") - , ("uvvs", "video/vnd.dece.sd") - , ("uvvt", "application/vnd.dece.ttml+xml") - , ("uvvu", "video/vnd.uvvu.mp4") - , ("uvvv", "video/vnd.dece.video") - , ("uvvx", "application/vnd.dece.unspecified") - , ("uvvz", "application/vnd.dece.zip") - , ("uvx", "application/vnd.dece.unspecified") - , ("uvz", "application/vnd.dece.zip") - , ("vcard", "text/vcard") - , ("vcd", "application/x-cdlink") - , ("vcf", "text/x-vcard") - , ("vcg", "application/vnd.groove-vcard") - , ("vcs", "text/x-vcalendar") - , ("vcx", "application/vnd.vcx") - , ("vis", "application/vnd.visionary") - , ("viv", "video/vnd.vivo") - , ("vob", "video/x-ms-vob") - , ("vor", "application/vnd.stardivision.writer") - , ("vox", "application/x-authorware-bin") - , ("vrml", "model/vrml") - , ("vsd", "application/vnd.visio") - , ("vsf", "application/vnd.vsf") - , ("vss", "application/vnd.visio") - , ("vst", "application/vnd.visio") - , ("vsw", "application/vnd.visio") - , ("vtu", "model/vnd.vtu") - , ("vxml", "application/voicexml+xml") - , ("w3d", "application/x-director") - , ("wad", "application/x-doom") - , ("wav", "audio/x-wav") - , ("wax", "audio/x-ms-wax") - , ("wbmp", "image/vnd.wap.wbmp") - , ("wbs", "application/vnd.criticaltools.wbs+xml") - , ("wbxml", "application/vnd.wap.wbxml") - , ("wcm", "application/vnd.ms-works") - , ("wdb", "application/vnd.ms-works") - , ("wdp", "image/vnd.ms-photo") - , ("weba", "audio/webm") - , ("webm", "video/webm") - , ("webp", "image/webp") - , ("wg", "application/vnd.pmi.widget") - , ("wgt", "application/widget") - , ("wks", "application/vnd.ms-works") - , ("wm", "video/x-ms-wm") - , ("wma", "audio/x-ms-wma") - , ("wmd", "application/x-ms-wmd") - , ("wmf", "application/x-msmetafile") - , ("wml", "text/vnd.wap.wml") - , ("wmlc", "application/vnd.wap.wmlc") - , ("wmls", "text/vnd.wap.wmlscript") - , ("wmlsc", "application/vnd.wap.wmlscriptc") - , ("wmv", "video/x-ms-wmv") - , ("wmx", "video/x-ms-wmx") - , ("wmz", "application/x-ms-wmz") - , ("wmz", "application/x-msmetafile") - , ("woff", "font/woff") - , ("woff2", "font/woff2") - , ("wpd", "application/vnd.wordperfect") - , ("wpl", "application/vnd.ms-wpl") - , ("wps", "application/vnd.ms-works") - , ("wqd", "application/vnd.wqd") - , ("wri", "application/x-mswrite") - , ("wrl", "model/vrml") - , ("wsdl", "application/wsdl+xml") - , ("wspolicy", "application/wspolicy+xml") - , ("wtb", "application/vnd.webturbo") - , ("wvx", "video/x-ms-wvx") - , ("x32", "application/x-authorware-bin") - , ("x3d", "model/x3d+xml") - , ("x3db", "model/x3d+binary") - , ("x3dbz", "model/x3d+binary") - , ("x3dv", "model/x3d+vrml") - , ("x3dvz", "model/x3d+vrml") - , ("x3dz", "model/x3d+xml") - , ("xaml", "application/xaml+xml") - , ("xap", "application/x-silverlight-app") - , ("xar", "application/vnd.xara") - , ("xbap", "application/x-ms-xbap") - , ("xbd", "application/vnd.fujixerox.docuworks.binder") - , ("xbm", "image/x-xbitmap") - , ("xdf", "application/xcap-diff+xml") - , ("xdm", "application/vnd.syncml.dm+xml") - , ("xdp", "application/vnd.adobe.xdp+xml") - , ("xdssc", "application/dssc+xml") - , ("xdw", "application/vnd.fujixerox.docuworks") - , ("xenc", "application/xenc+xml") - , ("xer", "application/patch-ops-error+xml") - , ("xfdf", "application/vnd.adobe.xfdf") - , ("xfdl", "application/vnd.xfdl") - , ("xht", "application/xhtml+xml") - , ("xhtml", "application/xhtml+xml") - , ("xhvml", "application/xv+xml") - , ("xif", "image/vnd.xiff") - , ("xla", "application/vnd.ms-excel") - , ("xlam", "application/vnd.ms-excel.addin.macroenabled.12") - , ("xlc", "application/vnd.ms-excel") - , ("xlf", "application/x-xliff+xml") - , ("xlm", "application/vnd.ms-excel") - , ("xls", "application/vnd.ms-excel") - , ("xlsb", "application/vnd.ms-excel.sheet.binary.macroenabled.12") - , ("xlsm", "application/vnd.ms-excel.sheet.macroenabled.12") - , ("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") - , ("xlt", "application/vnd.ms-excel") - , ("xltm", "application/vnd.ms-excel.template.macroenabled.12") - , ("xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template") - , ("xlw", "application/vnd.ms-excel") - , ("xm", "audio/xm") - , ("xml", "application/xml") - , ("xo", "application/vnd.olpc-sugar") - , ("xop", "application/xop+xml") - , ("xpi", "application/x-xpinstall") - , ("xpl", "application/xproc+xml") - , ("xpm", "image/x-xpixmap") - , ("xpr", "application/vnd.is-xpr") - , ("xps", "application/vnd.ms-xpsdocument") - , ("xpw", "application/vnd.intercon.formnet") - , ("xpx", "application/vnd.intercon.formnet") - , ("xsl", "application/xml") - , ("xslt", "application/xslt+xml") - , ("xsm", "application/vnd.syncml+xml") - , ("xspf", "application/xspf+xml") - , ("xul", "application/vnd.mozilla.xul+xml") - , ("xvm", "application/xv+xml") - , ("xvml", "application/xv+xml") - , ("xwd", "image/x-xwindowdump") - , ("xyz", "chemical/x-xyz") - , ("xz", "application/x-xz") - , ("yang", "application/yang") - , ("yin", "application/yin+xml") - , ("z1", "application/x-zmachine") - , ("z2", "application/x-zmachine") - , ("z3", "application/x-zmachine") - , ("z4", "application/x-zmachine") - , ("z5", "application/x-zmachine") - , ("z6", "application/x-zmachine") - , ("z7", "application/x-zmachine") - , ("z8", "application/x-zmachine") - , ("zaz", "application/vnd.zzazz.deck+xml") - , ("zip", "application/zip") - , ("zir", "application/vnd.zul") - , ("zirz", "application/vnd.zul") - , ("zmm", "application/vnd.handheld-entertainment+xml") - ] \ No newline at end of file diff --git a/IHP/Mail.hs b/IHP/Mail.hs index d1393b30d..ce175023c 100644 --- a/IHP/Mail.hs +++ b/IHP/Mail.hs @@ -54,7 +54,7 @@ sendWithMailServer SES { .. } mail = do manager <- Network.HTTP.Client.newManager Network.HTTP.Client.TLS.tlsManagerSettings let ses = Mailer.SES { Mailer.sesFrom = cs $ addressEmail (mailFrom mail), - Mailer.sesTo = map (cs . addressEmail) (mailTo mail), + Mailer.sesTo = map (cs . addressEmail) ((mailTo mail) <> (mailCc mail) <> (mailBcc mail)), Mailer.sesAccessKey = accessKey, Mailer.sesSecretKey = secretKey, Mailer.sesSessionToken = Nothing, diff --git a/IHP/SchemaMigration.hs b/IHP/SchemaMigration.hs index 942ed67ae..22ac3d645 100644 --- a/IHP/SchemaMigration.hs +++ b/IHP/SchemaMigration.hs @@ -12,6 +12,7 @@ import qualified Data.Text.IO as Text import IHP.ModelSupport hiding (withTransaction) import qualified Data.Char as Char import IHP.Log.Types +import IHP.EnvVar data Migration = Migration { revision :: Int @@ -37,7 +38,9 @@ migrate options = do -- All queries are executed inside a database transaction to make sure that it can be restored when something goes wrong. runMigration :: (?modelContext :: ModelContext) => Migration -> IO () runMigration migration@Migration { revision, migrationFile } = do - migrationSql <- Text.readFile (cs $ migrationPath migration) + -- | User can specify migrations directory as environment variable (defaults to /Application/Migrations/...) + migrationFilePath <- migrationPath migration + migrationSql <- Text.readFile (cs migrationFilePath) let fullSql = [trimming| BEGIN; @@ -96,7 +99,8 @@ findMigratedRevisions = map (\[revision] -> revision) <$> sqlQuery "SELECT revis -- The result is sorted so that the oldest revision is first. findAllMigrations :: IO [Migration] findAllMigrations = do - directoryFiles <- Directory.listDirectory "Application/Migration" + migrationDir <- detectMigrationDir + directoryFiles <- Directory.listDirectory (cs migrationDir) directoryFiles |> map cs |> filter (\path -> ".sql" `isSuffixOf` path) @@ -123,5 +127,12 @@ pathToMigration fileName = case revision of |> fmap textToInt |> join -migrationPath :: Migration -> Text -migrationPath Migration { migrationFile } = "Application/Migration/" <> migrationFile +migrationPath :: Migration -> IO Text +migrationPath Migration { migrationFile } = do + migrationDir <- detectMigrationDir + pure (migrationDir <> migrationFile) + +detectMigrationDir :: IO Text +detectMigrationDir = + envOrDefault "IHP_MIGRATION_DIR" "Application/Migration/" + diff --git a/NixSupport/nixosModules/options.nix b/NixSupport/nixosModules/options.nix index 952d571f6..310bc1d8e 100644 --- a/NixSupport/nixosModules/options.nix +++ b/NixSupport/nixosModules/options.nix @@ -13,63 +13,70 @@ with lib; type = types.str; default = "https://${config.services.ihp.domain}"; }; - + migrations = mkOption { type = types.path; }; - + schema = mkOption { type = types.path; }; - + fixtures = mkOption { type = types.path; }; - + httpsEnabled = mkOption { type = types.bool; default = true; }; - + databaseName = mkOption { type = types.str; default = "app"; }; - + databaseUser = mkOption { type = types.str; default = "ihp"; }; - + databaseUrl = mkOption { type = types.str; }; - + # https://ihp.digitallyinduced.com/Guide/database-migrations.html#skipping-old-migrations minimumRevision = mkOption { type = types.int; default = 0; }; - + + # https://ihp.digitallyinduced.com/Guide/database-migrations.html#ihp-migrations-dir + ihpMigrationDir = mkOption { + type = types.str; + default = "Application/Migration/"; + }; + + ihpEnv = mkOption { type = types.str; default = "Production"; }; - + appPort = mkOption { type = types.int; default = 8000; }; - + requestLoggerIPAddrSource = mkOption { type = types.str; default = "FromHeader"; }; - + sessionSecret = mkOption { type = types.str; }; - + additionalEnvVars = mkOption { type = types.attrs; default = {}; @@ -79,7 +86,7 @@ with lib; type = types.package; default = if config.services.ihp.optimized then self.packages."${pkgs.system}".optimized-prod-server else self.packages."${pkgs.system}".default; }; - + optimized = mkOption { type = types.bool; default = false; @@ -91,4 +98,3 @@ with lib; }; }; } - diff --git a/NixSupport/nixosModules/services/migrate.nix b/NixSupport/nixosModules/services/migrate.nix index a5f6d9ad5..6d7f3895c 100644 --- a/NixSupport/nixosModules/services/migrate.nix +++ b/NixSupport/nixosModules/services/migrate.nix @@ -2,26 +2,15 @@ let cfg = config.services.ihp; in { - systemd.services.migrate = - let migrateApp = pkgs.stdenv.mkDerivation { - name = "migrate-app"; - src = cfg.migrations; - buildPhase = '' - mkdir -p $out/Application/Migration - find "$src" -mindepth 1 -type f -exec cp {} $out/Application/Migration \; - ''; - }; - in { - serviceConfig = { - Type = "oneshot"; - }; - script = '' - cd ${migrateApp} - ${ihp.apps."${pkgs.system}".migrate.program} - ''; - environment = { - DATABASE_URL = cfg.databaseUrl; - MINIMUM_REVISION = "${toString cfg.minimumRevision}"; - }; + systemd.services.migrate = { + serviceConfig = { + Type = "oneshot"; + ExecStart = ihp.apps."${pkgs.system}".migrate.program; + }; + environment = { + DATABASE_URL = cfg.databaseUrl; + MINIMUM_REVISION = "${toString cfg.minimumRevision}"; + IHP_MIGRATION_DIR = cfg.migrations; + }; }; -} \ No newline at end of file +} diff --git a/README.md b/README.md index 66a463077..bdb43dd0e 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ + + + +

diff --git a/Test/FileStorage/MimeTypesSpec.hs b/Test/FileStorage/MimeTypesSpec.hs deleted file mode 100644 index ac15242d1..000000000 --- a/Test/FileStorage/MimeTypesSpec.hs +++ /dev/null @@ -1,20 +0,0 @@ -{-| -Module: Test.FileStorage.MimeTypesSpec -Copyright: (c) digitally induced GmbH, 2021 --} -module Test.FileStorage.MimeTypesSpec where - -import Test.Hspec -import IHP.Prelude -import IHP.FileStorage.MimeTypes - -tests = do - describe "IHP.FileStorage.MimeTypes" do - describe "guessMimeType" do - it "return common mime types" do - guessMimeType "test.jpg" `shouldBe` "image/jpeg" - guessMimeType "test.txt" `shouldBe` "text/plain" - it "returns application/octet-stream for unknown files" do - guessMimeType "unknown" `shouldBe` "application/octet-stream" - guessMimeType "test.hztr43ed" `shouldBe` "application/octet-stream" - guessMimeType "" `shouldBe` "application/octet-stream" \ No newline at end of file diff --git a/Test/Main.hs b/Test/Main.hs index 7a03cfb2c..5e1f48ff8 100644 --- a/Test/Main.hs +++ b/Test/Main.hs @@ -45,7 +45,6 @@ import qualified Test.ViewSupportSpec import qualified Test.ServerSideComponent.HtmlParserSpec import qualified Test.ServerSideComponent.HtmlDiffSpec import qualified Test.FileStorage.ControllerFunctionsSpec -import qualified Test.FileStorage.MimeTypesSpec import qualified Test.DataSync.DynamicQueryCompiler import qualified Test.IDE.CodeGeneration.MigrationGenerator import qualified Test.PGListenerSpec @@ -80,7 +79,6 @@ main = hspec do Test.ServerSideComponent.HtmlParserSpec.tests Test.ServerSideComponent.HtmlDiffSpec.tests Test.FileStorage.ControllerFunctionsSpec.tests - Test.FileStorage.MimeTypesSpec.tests Test.DataSync.DynamicQueryCompiler.tests Test.IDE.SchemaDesigner.SchemaOperationsSpec.tests Test.IDE.CodeGeneration.MigrationGenerator.tests diff --git a/ihp-datasync-typescript/IHP/DataSync/TypeScript/Compiler.hs b/ihp-datasync-typescript/IHP/DataSync/TypeScript/Compiler.hs index c59539967..1656e37e2 100644 --- a/ihp-datasync-typescript/IHP/DataSync/TypeScript/Compiler.hs +++ b/ihp-datasync-typescript/IHP/DataSync/TypeScript/Compiler.hs @@ -259,6 +259,8 @@ declare module 'ihp-datasync/react' { */ function useQuery(queryBuilder: QueryBuilder, options?: DataSubscriptionOptions): Array | null; + function useCount
(queryBuilder: QueryBuilder): number | null; + /** * A version of `useQuery` when you only want to fetch a single record. * diff --git a/ihp-datasync-typescript/Test/Spec.hs b/ihp-datasync-typescript/Test/Spec.hs index e9061dcb2..c11af8acf 100644 --- a/ihp-datasync-typescript/Test/Spec.hs +++ b/ihp-datasync-typescript/Test/Spec.hs @@ -349,6 +349,8 @@ tests = do */ function useQuery
(queryBuilder: QueryBuilder, options?: DataSubscriptionOptions): Array | null; + function useCount
(queryBuilder: QueryBuilder): number | null; + /** * A version of `useQuery` when you only want to fetch a single record. * diff --git a/ihp-ide/IHP/IDE/SchemaDesigner/Controller/Migrations.hs b/ihp-ide/IHP/IDE/SchemaDesigner/Controller/Migrations.hs index 41f1d0d81..99074aa65 100644 --- a/ihp-ide/IHP/IDE/SchemaDesigner/Controller/Migrations.hs +++ b/ihp-ide/IHP/IDE/SchemaDesigner/Controller/Migrations.hs @@ -61,7 +61,7 @@ instance Controller MigrationsController where let errorMessage = case fromException exception of Just (exception :: EnhancedSqlError) -> cs exception.sqlError.sqlErrorMsg Nothing -> tshow exception - + setErrorMessage errorMessage redirectTo MigrationsAction Right _ -> do @@ -79,14 +79,14 @@ instance Controller MigrationsController where action UpdateMigrationAction { migrationId } = do migration <- findMigrationByRevision migrationId let sqlStatements = param "sqlStatements" - - Text.writeFile (cs $ SchemaMigration.migrationPath migration) sqlStatements + migrationFilePath <- SchemaMigration.migrationPath migration + Text.writeFile (cs migrationFilePath) sqlStatements redirectTo MigrationsAction action DeleteMigrationAction { migrationId } = do migration <- findMigrationByRevision migrationId - let path = cs $ SchemaMigration.migrationPath migration + path <- cs <$> SchemaMigration.migrationPath migration Directory.removeFile path @@ -101,7 +101,7 @@ instance Controller MigrationsController where let errorMessage = case fromException exception of Just (exception :: EnhancedSqlError) -> cs exception.sqlError.sqlErrorMsg Nothing -> tshow exception - + setErrorMessage errorMessage redirectTo MigrationsAction Right _ -> do @@ -109,7 +109,9 @@ instance Controller MigrationsController where redirectTo MigrationsAction readSqlStatements :: SchemaMigration.Migration -> IO Text -readSqlStatements migration = Text.readFile (cs $ SchemaMigration.migrationPath migration) +readSqlStatements migration = do + migrationFilePath <- (SchemaMigration.migrationPath migration) + Text.readFile (cs migrationFilePath) findRecentMigrations :: IO [SchemaMigration.Migration] findRecentMigrations = take 20 . reverse <$> SchemaMigration.findAllMigrations diff --git a/ihp-ide/IHP/IDE/SchemaDesigner/View/Layout.hs b/ihp-ide/IHP/IDE/SchemaDesigner/View/Layout.hs index 2105f17cc..fe79ad6c3 100644 --- a/ihp-ide/IHP/IDE/SchemaDesigner/View/Layout.hs +++ b/ihp-ide/IHP/IDE/SchemaDesigner/View/Layout.hs @@ -404,7 +404,7 @@ renderColumn Column { name, columnType, defaultValue, notNull, isUnique } id tab Just value -> [hsx|default: {compileExpression value} |] Nothing -> mempty renderForeignKey = case findForeignKey statements tableName name of - Just addConstraint@AddConstraint { constraint = ForeignKeyConstraint { name = Just constraintName, referenceTable } } -> [hsx|FOREIGN KEY: {referenceTable}|] + Just addConstraint@AddConstraint { constraint = ForeignKeyConstraint { name = Just constraintName, referenceTable, onDelete = onDeleteConstraint } } -> [hsx|FOREIGN KEY: {referenceTable} (On Delete: {tshow onDeleteConstraint})|] _ -> mempty foreignKeyOption = case findForeignKey statements tableName name of Just addConstraint@AddConstraint { constraint = ForeignKeyConstraint { name = Just constraintName, referenceTable } } -> diff --git a/ihp.cabal b/ihp.cabal index b27059ae8..dc1996be4 100644 --- a/ihp.cabal +++ b/ihp.cabal @@ -101,6 +101,7 @@ common shared-properties , with-utf8 , ihp-hsx , ihp-postgresql-simple-extra + , mime-types default-extensions: OverloadedStrings , NoImplicitPrelude @@ -261,7 +262,6 @@ library , IHP.FileStorage.ControllerFunctions , IHP.FileStorage.Config , IHP.FileStorage.Preprocessor.ImageMagick - , IHP.FileStorage.MimeTypes , IHP.Pagination.ControllerFunctions , IHP.Pagination.ViewFunctions , IHP.Pagination.Types diff --git a/lib/IHP/DataSync/ihp-datasync.js b/lib/IHP/DataSync/ihp-datasync.js index 345834fb2..d568e6727 100644 --- a/lib/IHP/DataSync/ihp-datasync.js +++ b/lib/IHP/DataSync/ihp-datasync.js @@ -352,6 +352,11 @@ class DataSubscription { return; } + // Set isClosed early as we need to prevent a second close() from triggering another DeleteDataSubscription message + // also we don't want to receive any further messages, and onMessage will not process if isClosed == true + this.isClosed = true; + this.onClose(); + const dataSyncController = DataSyncController.getInstance(); const { subscriptionId } = await dataSyncController.sendMessage({ tag: 'DeleteDataSubscription', subscriptionId: this.subscriptionId }); @@ -360,10 +365,7 @@ class DataSubscription { dataSyncController.removeEventListener('reconnect', this.onDataSyncReconnect); dataSyncController.dataSubscriptions.splice(dataSyncController.dataSubscriptions.indexOf(this), 1); - this.isClosed = true; this.isConnected = false; - - this.onClose(); } onDataSyncClosed() { @@ -425,7 +427,9 @@ class DataSubscription { return () => { this.subscribers.splice(this.subscribers.indexOf(callback), 1); - this.closeIfNotUsed(); + // We delay the close as react could be re-rendering a component + // we garbage collect this connecetion once it's clearly not used anymore + setTimeout(this.closeIfNotUsed.bind(this), 1000); } } diff --git a/lib/IHP/DataSync/react.js b/lib/IHP/DataSync/react.js index ba008e110..311f338c8 100644 --- a/lib/IHP/DataSync/react.js +++ b/lib/IHP/DataSync/react.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useContext, useSyncExternalStore } from 'react'; +import React, { useState, useEffect, useContext, useSyncExternalStore, useRef, useMemo } from 'react'; import { DataSubscription, DataSyncController } from './ihp-datasync.js'; // Most IHP apps never use this context because they use session cookies for auth. @@ -99,4 +99,43 @@ export class DataSubscriptionStore { return subscription; } } +} + +export function useCount(queryBuilder) { + const count = useRef(null); + const getSnapshot = useMemo(() => () => count.current, []); + const subscribe = useMemo(() => (onStoreChange) => { + const controller = DataSyncController.getInstance(); + var isActive = true; + var subscriptionId = null; + const onMessage = (message) => { + if (message.tag === 'DidChangeCount' && message.subscriptionId === subscriptionId) { + count.current = message.count; + onStoreChange(); + } + }; + controller.sendMessage({ tag: 'CreateCountSubscription', query: queryBuilder.query }) + .then((response) => { + if (isActive) { + subscriptionId = response.subscriptionId; + count.current = response.count; + onStoreChange(); + + controller.addEventListener('message', onMessage); + } else { + controller.sendMessage({ tag: 'DeleteDataSubscription', subscriptionId: response.subscriptionId }); + } + }) + + return () => { + isActive = false; + + if (subscriptionId) { + controller.sendMessage({ tag: 'DeleteDataSubscription', subscriptionId }); + } + controller.removeEventListener('message', onMessage); + } + }, [JSON.stringify(queryBuilder.query)]); + + return useSyncExternalStore(subscribe, getSnapshot); } \ No newline at end of file