packages feed

supernova (empty) → 0.0.1

raw patch · 19 files changed

+2339/−0 lines, 19 filesdep +aesondep +asyncdep +basebuild-type:Customsetup-changed

Dependencies added: aeson, async, base, bifunctor, binary, bytestring, crc32c, exceptions, lens-family-core, logging, managed, network, proto-lens, proto-lens-runtime, streamly, supernova, text, unliftio

Files

+ LICENSE view
@@ -0,0 +1,201 @@+                                 Apache License+                           Version 2.0, January 2004+                        http://www.apache.org/licenses/++   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION++   1. Definitions.++      "License" shall mean the terms and conditions for use, reproduction,+      and distribution as defined by Sections 1 through 9 of this document.++      "Licensor" shall mean the copyright owner or entity authorized by+      the copyright owner that is granting the License.++      "Legal Entity" shall mean the union of the acting entity and all+      other entities that control, are controlled by, or are under common+      control with that entity. For the purposes of this definition,+      "control" means (i) the power, direct or indirect, to cause the+      direction or management of such entity, whether by contract or+      otherwise, or (ii) ownership of fifty percent (50%) or more of the+      outstanding shares, or (iii) beneficial ownership of such entity.++      "You" (or "Your") shall mean an individual or Legal Entity+      exercising permissions granted by this License.++      "Source" form shall mean the preferred form for making modifications,+      including but not limited to software source code, documentation+      source, and configuration files.++      "Object" form shall mean any form resulting from mechanical+      transformation or translation of a Source form, including but+      not limited to compiled object code, generated documentation,+      and conversions to other media types.++      "Work" shall mean the work of authorship, whether in Source or+      Object form, made available under the License, as indicated by a+      copyright notice that is included in or attached to the work+      (an example is provided in the Appendix below).++      "Derivative Works" shall mean any work, whether in Source or Object+      form, that is based on (or derived from) the Work and for which the+      editorial revisions, annotations, elaborations, or other modifications+      represent, as a whole, an original work of authorship. For the purposes+      of this License, Derivative Works shall not include works that remain+      separable from, or merely link (or bind by name) to the interfaces of,+      the Work and Derivative Works thereof.++      "Contribution" shall mean any work of authorship, including+      the original version of the Work and any modifications or additions+      to that Work or Derivative Works thereof, that is intentionally+      submitted to Licensor for inclusion in the Work by the copyright owner+      or by an individual or Legal Entity authorized to submit on behalf of+      the copyright owner. For the purposes of this definition, "submitted"+      means any form of electronic, verbal, or written communication sent+      to the Licensor or its representatives, including but not limited to+      communication on electronic mailing lists, source code control systems,+      and issue tracking systems that are managed by, or on behalf of, the+      Licensor for the purpose of discussing and improving the Work, but+      excluding communication that is conspicuously marked or otherwise+      designated in writing by the copyright owner as "Not a Contribution."++      "Contributor" shall mean Licensor and any individual or Legal Entity+      on behalf of whom a Contribution has been received by Licensor and+      subsequently incorporated within the Work.++   2. Grant of Copyright License. Subject to the terms and conditions of+      this License, each Contributor hereby grants to You a perpetual,+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable+      copyright license to reproduce, prepare Derivative Works of,+      publicly display, publicly perform, sublicense, and distribute the+      Work and such Derivative Works in Source or Object form.++   3. Grant of Patent License. Subject to the terms and conditions of+      this License, each Contributor hereby grants to You a perpetual,+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable+      (except as stated in this section) patent license to make, have made,+      use, offer to sell, sell, import, and otherwise transfer the Work,+      where such license applies only to those patent claims licensable+      by such Contributor that are necessarily infringed by their+      Contribution(s) alone or by combination of their Contribution(s)+      with the Work to which such Contribution(s) was submitted. If You+      institute patent litigation against any entity (including a+      cross-claim or counterclaim in a lawsuit) alleging that the Work+      or a Contribution incorporated within the Work constitutes direct+      or contributory patent infringement, then any patent licenses+      granted to You under this License for that Work shall terminate+      as of the date such litigation is filed.++   4. Redistribution. You may reproduce and distribute copies of the+      Work or Derivative Works thereof in any medium, with or without+      modifications, and in Source or Object form, provided that You+      meet the following conditions:++      (a) You must give any other recipients of the Work or+          Derivative Works a copy of this License; and++      (b) You must cause any modified files to carry prominent notices+          stating that You changed the files; and++      (c) You must retain, in the Source form of any Derivative Works+          that You distribute, all copyright, patent, trademark, and+          attribution notices from the Source form of the Work,+          excluding those notices that do not pertain to any part of+          the Derivative Works; and++      (d) If the Work includes a "NOTICE" text file as part of its+          distribution, then any Derivative Works that You distribute must+          include a readable copy of the attribution notices contained+          within such NOTICE file, excluding those notices that do not+          pertain to any part of the Derivative Works, in at least one+          of the following places: within a NOTICE text file distributed+          as part of the Derivative Works; within the Source form or+          documentation, if provided along with the Derivative Works; or,+          within a display generated by the Derivative Works, if and+          wherever such third-party notices normally appear. The contents+          of the NOTICE file are for informational purposes only and+          do not modify the License. You may add Your own attribution+          notices within Derivative Works that You distribute, alongside+          or as an addendum to the NOTICE text from the Work, provided+          that such additional attribution notices cannot be construed+          as modifying the License.++      You may add Your own copyright statement to Your modifications and+      may provide additional or different license terms and conditions+      for use, reproduction, or distribution of Your modifications, or+      for any such Derivative Works as a whole, provided Your use,+      reproduction, and distribution of the Work otherwise complies with+      the conditions stated in this License.++   5. Submission of Contributions. Unless You explicitly state otherwise,+      any Contribution intentionally submitted for inclusion in the Work+      by You to the Licensor shall be under the terms and conditions of+      this License, without any additional terms or conditions.+      Notwithstanding the above, nothing herein shall supersede or modify+      the terms of any separate license agreement you may have executed+      with Licensor regarding such Contributions.++   6. Trademarks. This License does not grant permission to use the trade+      names, trademarks, service marks, or product names of the Licensor,+      except as required for reasonable and customary use in describing the+      origin of the Work and reproducing the content of the NOTICE file.++   7. Disclaimer of Warranty. Unless required by applicable law or+      agreed to in writing, Licensor provides the Work (and each+      Contributor provides its Contributions) on an "AS IS" BASIS,+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or+      implied, including, without limitation, any warranties or conditions+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A+      PARTICULAR PURPOSE. You are solely responsible for determining the+      appropriateness of using or redistributing the Work and assume any+      risks associated with Your exercise of permissions under this License.++   8. Limitation of Liability. In no event and under no legal theory,+      whether in tort (including negligence), contract, or otherwise,+      unless required by applicable law (such as deliberate and grossly+      negligent acts) or agreed to in writing, shall any Contributor be+      liable to You for damages, including any direct, indirect, special,+      incidental, or consequential damages of any character arising as a+      result of this License or out of the use or inability to use the+      Work (including but not limited to damages for loss of goodwill,+      work stoppage, computer failure or malfunction, or any and all+      other commercial damages or losses), even if such Contributor+      has been advised of the possibility of such damages.++   9. Accepting Warranty or Additional Liability. While redistributing+      the Work or Derivative Works thereof, You may choose to offer,+      and charge a fee for, acceptance of support, warranty, indemnity,+      or other liability obligations and/or rights consistent with this+      License. However, in accepting such obligations, You may act only+      on Your own behalf and on Your sole responsibility, not on behalf+      of any other Contributor, and only if You agree to indemnify,+      defend, and hold each Contributor harmless for any liability+      incurred by, or claims asserted against, such Contributor by reason+      of your accepting any such warranty or additional liability.++   END OF TERMS AND CONDITIONS++   APPENDIX: How to apply the Apache License to your work.++      To apply the Apache License to your work, attach the following+      boilerplate notice, with the fields enclosed by brackets "[]"+      replaced with your own identifying information. (Don't include+      the brackets!)  The text should be enclosed in the appropriate+      comment syntax for the file format. We also recommend that a+      file or class name and description of purpose be included on the+      same "printed page" as the copyright notice for easier+      identification within third-party archives.++   Copyright [yyyy] [name of copyright owner]++   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.
+ README.md view
@@ -0,0 +1,75 @@+supernova+=========++[![CI Status](https://github.com/cr-org/supernova/workflows/Haskell%20CI/badge.svg)](https://github.com/cr-org/supernova/actions)++⚠️  **it is still very much under development 🚧 so use it at your own risk** ⚠️++A *supernova* is a powerful and luminous stellar explosion. This transient astronomical event occurs during the last evolutionary stages of a massive star or when a white dwarf is triggered into runaway nuclear fusion. The original object, called the progenitor, either collapses to a [neutron](https://github.com/cr-org/neutron) star or black hole, or is completely destroyed. The peak optical luminosity of a supernova can be comparable to that of an entire galaxy before fading over several weeks or months.++[![supernova](https://www.jpl.nasa.gov/spaceimages/images/largesize/PIA22352_hires.jpg "Kepler Beyond Planets: Finding Exploding Stars (Type Ia Supernova from a White Dwarf Stealing Matter)")](https://www.jpl.nasa.gov/spaceimages/details.php?id=PIA22352)++### Quick Start++The example located in `test/Main.hs` showcases a simple consumer and producer running concurrently (needs the `async` library).++```haskell+main :: IO ()+main = runPulsar resources $ \(Consumer {..}, Producer {..}) ->+  let c = forever $ fetch >>= \(Message i m) -> msgDecoder m >> ack i+      p = forever $ sleep 5 >> traverse_ produce messages+  in  concurrently_ c p++resources :: Pulsar (Consumer IO, Producer IO)+resources = do+  ctx      <- connect defaultConnectData+  consumer <- newConsumer ctx topic "test-sub"+  producer <- newProducer ctx topic+  return (consumer, producer)+```++A `Message` contains a `MessageID` you need for `ack`ing and a payload defined as a lazy `ByteString`.++Run it with the following command:++```shell+cabal new-run supernova-tests+```++By default, it logs to the standard output in DEBUG level. You can change it by suppling `LogOptions`.++```haskell+logOpts :: LogOptions+logOpts = LogOptions Info StdOut++runPulsar' logOpts resources+```++### Streaming++Since both consumers and producers operate on any `MonadIO m`, we could leverage some streaming libraries. Here's the same example using [streamly](https://hackage.haskell.org/package/streamly).++```haskell+import           Streamly+import qualified Streamly.Prelude              as S++main :: IO ()+main = runPulsar resources $ \(Consumer {..}, Producer {..}) ->+  let c = forever $ fetch >>= \(Message i p) -> msgDecoder p >> ack i+      p = forever $ sleep 5 >> traverse_ produce messages+  in  S.drain . asyncly . maxThreads 10 $ S.yieldM c <> S.yieldM p+```++### Development++It is recommended to use [Cachix](https://app.cachix.org/cache/hpulsar) to reduce the compilation time.++```shell+nix-build+```++Or within a Nix shell (run `nix-shell` at the project's root).++```shell+cabal new-build+```
+ Setup.hs view
@@ -0,0 +1,3 @@+import Data.ProtoLens.Setup++main = defaultMainGeneratingProtos "src"
+ src/Pulsar.hs view
@@ -0,0 +1,61 @@+{-|+Module      : Pulsar+Description : Apache Pulsar client+License     : Apache-2.0+Maintainer  : gabriel.volpe@chatroulette.com+Stability   : experimental++Consider the following imports (needs the [async](http://hackage.haskell.org/package/async) library).++@+import           Control.Concurrent             ( threadDelay )+import           Control.Concurrent.Async       ( concurrently_ )+import           Control.Monad                  ( forever )+import           Pulsar+@++A quick example of a consumer and producer running concurrently.++@+resources :: Pulsar (Consumer IO, Producer IO)+resources = do+  ctx      <- connect defaultConnectData+  consumer <- newConsumer ctx topic "test-sub"+  producer <- newProducer ctx topic+  return (consumer, producer)+@++A Pulsar connection, consumers, and producers are long-lived resources that are managed accordingly for you. Once the program exits, the resources will be released in the respective order (always opposite to the order of acquisition).++@+main :: IO ()+main = runPulsar resources $ \(Consumer {..}, Producer {..}) ->+  let c = forever $ fetch >>= \(Message i m) -> print m >> ack i+      p = forever $ threadDelay (5 * 1000000) >> produce "hello world"+  in  concurrently_ c p+@+-}+module Pulsar+  ( connect+  , defaultConnectData+  , newConsumer+  , newProducer+  , runPulsar+  , runPulsar'+  , Consumer(..)+  , Producer(..)+  , Pulsar+  , PulsarCtx+  , ConnectData+  , LogLevel(..)+  , LogOptions(..)+  , LogOutput(..)+  , module Pulsar.Types+  )+where++import           Pulsar.Connection+import           Pulsar.Consumer+import           Pulsar.Internal.Core+import           Pulsar.Producer+import           Pulsar.Types
+ src/Pulsar/Connection.hs view
@@ -0,0 +1,156 @@+{-# LANGUAGE GeneralizedNewtypeDeriving, LambdaCase, OverloadedStrings #-}++module Pulsar.Connection where++import           Control.Monad                  ( forever )+import           Control.Monad.Catch            ( MonadThrow )+import           Control.Monad.Managed+import qualified Data.Binary                   as B+import           Data.Foldable                  ( traverse_ )+import           Data.IORef+import           Lens.Family+import qualified Network.Socket                as NS+import qualified Network.Socket.ByteString.Lazy+                                               as SBL+import           Proto.PulsarApi                ( BaseCommand+                                                , MessageMetadata+                                                )+import qualified Proto.PulsarApi_Fields        as F+import           Pulsar.Internal.Logger+import           Pulsar.Internal.TCPClient      ( acquireSocket )+import qualified Pulsar.Protocol.Commands      as P+import           Pulsar.Protocol.Decoder        ( decodeBaseCommand )+import           Pulsar.Protocol.Encoder        ( encodeBaseCommand )+import           Pulsar.Protocol.Frame          ( Payload+                                                , Response(..)+                                                , frameMaxSize+                                                , getCommand+                                                )+import           System.Timeout                 ( timeout )+import           UnliftIO.Async                 ( concurrently_ )+import           UnliftIO.Chan+import           UnliftIO.Concurrent            ( forkIO+                                                , killThread+                                                , threadDelay+                                                )+import           UnliftIO.Exception             ( bracket+                                                , throwIO+                                                )++newtype Connection = Conn NS.Socket++newtype ReqId = ReqId B.Word64 deriving (Num, Show)+newtype SeqId = SeqId B.Word64 deriving (Num, Show)+newtype ProducerId = PId B.Word64 deriving (Num, Show)+newtype ConsumerId = CId B.Word64 deriving (Num, Show)++data AppState = AppState+  { appConsumers :: [(ConsumerId, Chan Response)] -- a list of consumer identifiers associated with a communication channel+  , appConsumerId :: ConsumerId                   -- an incremental counter to assign unique consumer ids+  , appProducers :: [(ProducerId, Chan Response)] -- a list of producer identifiers associated with a communication channel+  , appProducerId :: ProducerId                   -- an incremental counter to assign unique producer ids+  , appRequestId :: ReqId                         -- an incremental counter to assign unique request ids for all commands+  }++mkConsumerId :: MonadIO m => Chan Response -> IORef AppState -> m ConsumerId+mkConsumerId chan ref = liftIO $ atomicModifyIORef+  ref+  (\(AppState cs cid ps pid rid) ->+    let cid' = cid + 1 in (AppState ((cid', chan) : cs) cid' ps pid rid, cid)+  )++mkProducerId :: MonadIO m => Chan Response -> IORef AppState -> m ProducerId+mkProducerId chan ref = liftIO $ atomicModifyIORef+  ref+  (\(AppState cs cid ps pid rid) ->+    let pid' = pid + 1 in (AppState cs cid ((pid', chan) : ps) pid' rid, pid)+  )++mkRequestId :: MonadIO m => IORef AppState -> m ReqId+mkRequestId ref = liftIO $ atomicModifyIORef+  ref+  (\(AppState cs cid ps pid req) ->+    let req' = req + 1 in (AppState cs cid ps pid req', req)+  )++{- | Connection details: host and port. -}+data ConnectData = ConnData+    { connHost :: NS.HostName+    , connPort :: NS.ServiceName+    } deriving Show++{- | Internal Pulsar context. You will never need to access its content (not exported) but might need to take it as argument. -}+data PulsarCtx = Ctx+  { ctxConn :: Connection+  , ctxState :: IORef AppState+  }++{- | Default connection data: "127.0.0.1:6650" -}+defaultConnectData :: ConnectData+defaultConnectData = ConnData { connHost = "127.0.0.1", connPort = "6650" }++{- | Starts a Pulsar connection with the supplied 'ConnectData' -}+connect+  :: (MonadThrow m, MonadIO m, MonadManaged m) => ConnectData -> m PulsarCtx+connect (ConnData h p) = do+  socket <- acquireSocket h p+  liftIO $ sendSimpleCmd socket P.connect+  resp <- receive socket+  case getCommand resp ^. F.maybe'connected of+    Just _  -> logResponse resp+    Nothing -> throwIO $ userError "Could not connect"+  app   <- liftIO initAppState+  kchan <- liftIO newChan+  let ctx        = Ctx (Conn socket) app+      dispatcher = recvDispatch socket app kchan+      task       = concurrently_ dispatcher (keepAlive socket kchan)+  using $ ctx <$ managed (bracket (forkIO task) killThread)++initAppState :: MonadIO m => m (IORef AppState)+initAppState = liftIO . newIORef $ AppState [] 0 [] 0 0++recvDispatch+  :: MonadIO m => NS.Socket -> IORef AppState -> Chan BaseCommand -> m ()+recvDispatch s ref chan = forever $ do+  resp                   <- receive s+  (AppState cs _ ps _ _) <- liftIO $ readIORef ref+  case getCommand resp ^. F.maybe'pong of+    Just _  -> writeChan chan (getCommand resp)+    Nothing -> traverse_ (`writeChan` resp) ((snd <$> cs) ++ (snd <$> ps))++{- Emit a PING and expect a PONG every 29 seconds. If a PONG is not received, interrupt connection -}+keepAlive :: MonadIO m => NS.Socket -> Chan BaseCommand -> m ()+keepAlive s chan = forever $ do+  threadDelay (29 * 1000000)+  logRequest P.ping+  sendSimpleCmd s P.ping+  liftIO $ timeout (2 * 1000000) (readChan chan) >>= \case+    Just cmd -> logResponse cmd+    Nothing  -> throwIO (userError "Keep Alive interruption")++sendSimpleCmd :: MonadIO m => NS.Socket -> BaseCommand -> m ()+sendSimpleCmd s cmd =+  liftIO . SBL.sendAll s $ encodeBaseCommand Nothing Nothing cmd++sendPayloadCmd+  :: MonadIO m+  => NS.Socket+  -> BaseCommand+  -> MessageMetadata+  -> Maybe Payload+  -> m ()+sendPayloadCmd s cmd meta payload =+  liftIO . SBL.sendAll s $ encodeBaseCommand (Just meta) payload cmd++receive :: MonadIO m => NS.Socket -> m Response+receive s = liftIO $ do+  msg <- SBL.recv s $ fromIntegral frameMaxSize+  case decodeBaseCommand msg of+    Left  e    -> fail $ "Decoding error: " <> e+    Right resp -> case getCommand resp ^. F.maybe'ping of+      Just _ -> do+        logResponse $ getCommand resp+        logRequest P.pong+        sendSimpleCmd s P.pong+        return resp+      Nothing -> return resp
+ src/Pulsar/Consumer.hs view
@@ -0,0 +1,60 @@+{-# LANGUAGE LambdaCase, OverloadedStrings #-}++module Pulsar.Consumer where++import           Control.Monad                  ( forever )+import qualified Control.Monad.Catch           as E+import           Control.Monad.Managed+import           Lens.Family+import qualified Proto.PulsarApi_Fields        as F+import qualified Pulsar.Core                   as C+import           Pulsar.Connection+import           Pulsar.Internal.Logger         ( logResponse )+import           Pulsar.Protocol.Frame          ( Payload(..)+                                                , Response(..)+                                                )+import           Pulsar.Types+import           UnliftIO.Chan+import           UnliftIO.Concurrent            ( forkIO+                                                , killThread+                                                )++{- | An abstract 'Consumer' able to 'fetch' messages and 'ack'nowledge them. -}+data Consumer m = Consumer+  { fetch :: m Message   -- ^ Fetches a single message. Blocks if no messages are available.+  , ack :: MsgId -> m () -- ^ Acknowledges a single message.+  }++{- | Create a new 'Consumer' by supplying a 'PulsarCtx' (returned by 'Pulsar.connect'), a 'Topic' and a 'SubscriptionName'. -}+newConsumer+  :: (MonadManaged m, MonadIO f)+  => PulsarCtx+  -> Topic+  -> SubscriptionName+  -> m (Consumer f)+newConsumer (Ctx conn app) topic sub = do+  chan  <- newChan+  cid   <- mkConsumerId chan app+  fchan <- newChan+  using $ Consumer (readChan fchan) (acker cid) <$ managed+    (E.bracket+      (mkSubscriber chan cid >> forkIO (fetcher chan fchan))+      (\i -> newReq >>= \r -> C.closeConsumer conn chan r cid >> killThread i)+    )+ where+  fetcher chan fc = liftIO . forever $ readChan chan >>= \case+    PayloadResponse cmd _ p -> case cmd ^. F.maybe'message of+      Just msg ->+        let msgId = msg ^. F.messageId+            pm    = Message (MsgId msgId) $ maybe "" (\(Payload x) -> x) p+        in  logResponse cmd >> writeChan fc pm+      Nothing -> return ()+    _ -> return ()+  newReq = mkRequestId app+  acker cid (MsgId mid) = liftIO $ C.ack conn cid mid+  mkSubscriber chan cid = do+    req1 <- newReq+    C.lookup conn chan req1 topic+    req2 <- newReq+    C.newSubscriber conn chan req2 cid topic sub+    C.flow conn cid
+ src/Pulsar/Core.hs view
@@ -0,0 +1,140 @@+{-# LANGUAGE DataKinds, FlexibleContexts, LambdaCase, OverloadedStrings #-}++{- Defines a set of transactional commands, communicating via internal channels -}+module Pulsar.Core where++import           Control.Exception              ( throwIO )+import           Control.Monad.Catch            ( MonadThrow )+import           Control.Monad.IO.Class+import qualified Data.Binary                   as B+import           Data.Functor                   ( void )+import           Data.ProtoLens.Field           ( HasField )+import           Data.Text                      ( Text )+import           Lens.Family+import           Proto.PulsarApi+import qualified Proto.PulsarApi_Fields        as F+import           Pulsar.Connection+import           Pulsar.Internal.Logger+import qualified Pulsar.Protocol.Commands      as P+import           Pulsar.Protocol.Frame          ( Payload(..)+                                                , Response(..)+                                                , getCommand+                                                )+import           Pulsar.Types+import           UnliftIO.Chan++------ Simple commands ------++verifyResponse+  :: (HasField a "requestId" B.Word64, Show a)+  => ReqId+  -> Chan Response+  -> LensLike' (Constant (Maybe a)) BaseCommand (Maybe a)+  -> IO (Maybe a)+verifyResponse r@(ReqId req) chan lens = do+  resp <- readChan chan+  let cmd'    = getCommand resp ^. lens+      req'    = view F.requestId <$> cmd'+      rewrite = writeChan chan resp+      loop    = verifyResponse r chan lens+      checkEq (_, rq) | rq == req = cmd' <$ logResponse resp+                      | otherwise = rewrite >> loop+  maybe loop checkEq $ (,) <$> cmd' <*> req'++lookup :: Connection -> Chan Response -> ReqId -> Topic -> IO ()+lookup (Conn s) chan r@(ReqId req) topic = do+  logRequest $ P.lookup req topic+  sendSimpleCmd s $ P.lookup req topic+  -- TODO: we need to analyze it and might need to re-issue another lookup+  void $ verifyResponse r chan F.maybe'lookupTopicResponse++newProducer+  :: Connection -> Chan Response -> ReqId -> ProducerId -> Topic -> IO Text+newProducer (Conn s) chan r@(ReqId req) (PId pid) topic = do+  logRequest $ P.producer req pid topic+  sendSimpleCmd s $ P.producer req pid topic+  verifyResponse r chan F.maybe'producerSuccess >>= \case+    Just ps -> return $ ps ^. F.producerName+    Nothing -> return ""++closeProducer :: Connection -> Chan Response -> ReqId -> ProducerId -> IO ()+closeProducer (Conn s) chan r@(ReqId req) (PId pid) = do+  logRequest $ P.closeProducer req pid+  sendSimpleCmd s $ P.closeProducer req pid+  void $ verifyResponse r chan F.maybe'success++newSubscriber+  :: Connection+  -> Chan Response+  -> ReqId+  -> ConsumerId+  -> Topic+  -> SubscriptionName+  -> IO ()+newSubscriber (Conn s) chan r@(ReqId req) (CId cid) topic subs = do+  logRequest $ P.subscribe req cid topic subs+  sendSimpleCmd s $ P.subscribe req cid topic subs+  -- TODO: we may need to check for failure too+  void $ verifyResponse r chan F.maybe'success++flow :: Connection -> ConsumerId -> IO ()+flow (Conn s) (CId cid) = do+  logRequest $ P.flow cid+  sendSimpleCmd s $ P.flow cid++ack :: MonadIO m => Connection -> ConsumerId -> MessageIdData -> m ()+ack (Conn s) (CId cid) msgId = do+  logRequest $ P.ack cid msgId+  sendSimpleCmd s $ P.ack cid msgId++closeConsumer :: Connection -> Chan Response -> ReqId -> ConsumerId -> IO ()+closeConsumer (Conn s) _ (ReqId req) (CId cid) = do+  logRequest $ P.closeConsumer req cid+  sendSimpleCmd s $ P.closeConsumer req cid+  -- FIXME: this is a workaround but the problem is the response for close consumer never comes on a SIGTERM when consuming+  -- from the Chan, since the writer gets interrupted and no messages come in.+  resp <- receive s+  case getCommand resp ^. F.maybe'success of+    Just _  -> logResponse resp+    Nothing -> return ()++------ Keep Alive -------++ping :: (MonadThrow m, MonadIO m) => Connection -> Chan Response -> m ()+ping (Conn s) chan = do+  logRequest P.ping+  sendSimpleCmd s P.ping+  cmd <- getCommand <$> readChan chan+  case cmd ^. F.maybe'pong of+    Just p  -> logResponse p+    Nothing -> liftIO . throwIO $ userError "Failed to get PONG"++pong :: MonadIO m => Connection -> m ()+pong (Conn s) = do+  logRequest P.pong+  sendSimpleCmd s P.pong++------ Payload commands ------++send+  :: Connection+  -> Chan Response+  -> ProducerId+  -> SeqId+  -> PulsarMessage+  -> IO ()+send (Conn s) chan (PId pid) (SeqId sid) (PulsarMessage msg) = do+  logRequest $ P.send pid sid+  sendPayloadCmd s (P.send pid sid) P.messageMetadata (Just $ Payload msg)+  confirmReception+ where+  confirmReception = do+    resp <- readChan chan+    let cmd'    = getCommand resp ^. F.maybe'sendReceipt+        pid'    = view F.producerId <$> cmd'+        sid'    = view F.sequenceId <$> cmd'+        rewrite = writeChan chan resp+        loop    = confirmReception+        checkEq (_, pd, sd) | pd == pid && sd == sid = logResponse resp+                            | otherwise              = rewrite >> loop+    maybe loop checkEq $ (,,) <$> cmd' <*> pid' <*> sid'
+ src/Pulsar/Internal/Core.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE RankNTypes, GeneralizedNewtypeDeriving #-}++{- Defines a Pulsar Monad, which wraps a Managed resource -}+module Pulsar.Internal.Core where++import qualified Control.Logging               as L+import           Control.Monad.Catch+import           Control.Monad.Managed++{- | The main Pulsar monad, which abstracts over a 'Managed' monad. -}+newtype Pulsar a = Pulsar (Managed a)+  deriving (Functor, Applicative, Monad, MonadIO, MonadManaged)++{- | Runs a Pulsar computation with default logging to standard output -}+runPulsar :: forall a b . Pulsar a -> (a -> IO b) -> IO b+runPulsar (Pulsar mgd) f = do+  L.setLogTimeFormat "%H:%M:%S%Q"+  L.withStdoutLogging $ with mgd f++{- | Runs a Pulsar computation with the supplied logging options -}+runPulsar' :: forall a b . LogOptions -> Pulsar a -> (a -> IO b) -> IO b+runPulsar' (LogOptions lvl out) (Pulsar mgd) f = do+  L.setLogLevel $ convertLogLevel lvl+  L.setLogTimeFormat "%H:%M:%S%Q"+  case out of+    StdOut  -> L.withStdoutLogging $ with mgd f+    File fp -> L.withFileLogging fp $ with mgd f++instance MonadThrow Pulsar where+  throwM = liftIO . throwM++{- | Internal logging options. Can be used together with `runPulsar'`. -}+data LogOptions = LogOptions+  { logLevel :: LogLevel+  , logOutput :: LogOutput+  } deriving Show++{- | Internal logging level, part of 'LogOptions'. Can be used together with `runPulsar'`. -}+data LogLevel = Error | Warn | Info | Debug deriving Show++{- | Internal logging output, part of 'LogOptions'. Can be used together with `runPulsar'`. -}+data LogOutput = StdOut | File FilePath deriving Show++convertLogLevel :: LogLevel -> L.LogLevel+convertLogLevel Error = L.LevelError+convertLogLevel Warn  = L.LevelWarn+convertLogLevel Info  = L.LevelInfo+convertLogLevel Debug = L.LevelDebug
+ src/Pulsar/Internal/Logger.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE OverloadedStrings #-}++{- Defines a few internal functions for logging requests and responses in debug mode -}+module Pulsar.Internal.Logger where++import           Control.Logging                ( debug' )+import           Control.Monad.IO.Class         ( MonadIO )+import qualified Data.Text                     as T++logRequest :: (MonadIO m, Show a) => a -> m ()+logRequest cmd = debug' $ ">>> " <> T.pack (show cmd)++logResponse :: (MonadIO m, Show a) => a -> m ()+logResponse cmd = debug' $ "<<< " <> T.pack (show cmd)
+ src/Pulsar/Internal/TCPClient.hs view
@@ -0,0 +1,31 @@+{-# LANGUAGE LambdaCase, OverloadedStrings #-}++{- A simple TCP client, used to communicate with the Pulsar server -}+module Pulsar.Internal.TCPClient+  ( acquireSocket+  )+where++import qualified Control.Exception             as E+import           Control.Monad.IO.Class+import           Control.Monad.Managed+import qualified Network.Socket                as NS++acquireSocket+  :: (MonadIO m, MonadManaged m) => NS.HostName -> NS.ServiceName -> m NS.Socket+acquireSocket host port = do+  addr <- liftIO resolve+  using $ managed+    (E.bracket+      (putStrLn "[ Establishing connection with Pulsar ]" >> open addr)+      (\s -> putStrLn "[ Closing Pulsar connection ]" >> NS.close s)+    )+ where+  resolve = do+    let hints = NS.defaultHints { NS.addrSocketType = NS.Stream }+    NS.getAddrInfo (Just hints) (Just host) (Just port) >>= \case+      [addr] -> pure addr+      _      -> E.ioError $ userError "Could not resolve socket address"+  open addr = E.bracketOnError (NS.openSocket addr) NS.close $ \sock -> do+    NS.connect sock $ NS.addrAddress addr+    return sock
+ src/Pulsar/Producer.hs view
@@ -0,0 +1,49 @@+{- Defines a high-level Pulsar producer for the end user -}+module Pulsar.Producer where++import qualified Control.Monad.Catch           as E+import           Control.Monad.Managed+import           Data.IORef+import           Data.Text                      ( Text )+import qualified Pulsar.Core                   as C+import           Pulsar.Connection+import           Pulsar.Types+import           UnliftIO.Chan++{- | An abstract 'Producer' able to 'produce' messages of type 'PulsarMessage'. -}+newtype Producer m = Producer+  { produce :: PulsarMessage -> m () -- ^ Produces a single message.+  }++data ProducerState = ProducerState+  { stSeqId :: SeqId -- an incremental message sequence counter+  , stName :: Text   -- a unique name+  }++mkSeqId :: MonadIO m => IORef ProducerState -> m SeqId+mkSeqId ref = liftIO $ atomicModifyIORef+  ref+  (\(ProducerState s n) -> let s' = s + 1 in (ProducerState s' n, s))++{- | Create a new 'Producer' by supplying a 'PulsarCtx' (returned by 'Pulsar.connect') and a 'Topic'. -}+newProducer+  :: (MonadManaged m, MonadIO f) => PulsarCtx -> Topic -> m (Producer f)+newProducer (Ctx conn app) topic = do+  chan  <- newChan+  pid   <- mkProducerId chan app+  pname <- liftIO $ mkProducer chan pid+  pst   <- liftIO $ newIORef (ProducerState 0 pname)+  using $ managed+    (E.bracket (pure $ Producer (dispatch chan pid pst))+               (const $ newReq >>= \r -> C.closeProducer conn chan r pid)+    )+ where+  newReq = mkRequestId app+  dispatch chan pid pst msg = do+    sid <- mkSeqId pst+    liftIO $ C.send conn chan pid sid msg+  mkProducer chan pid = do+    req1 <- newReq+    C.lookup conn chan req1 topic+    req2 <- newReq+    C.newProducer conn chan req2 pid topic
+ src/Pulsar/Protocol/Commands.hs view
@@ -0,0 +1,124 @@+{-# LANGUAGE OverloadedStrings #-}++{- Defines commands using the generated protobuf datatypes -}+module Pulsar.Protocol.Commands where++import qualified Data.Binary                   as B+import           Data.ProtoLens                 ( defMessage )+import qualified Data.Text                     as T+import           Data.Version                   ( showVersion )+import           Lens.Family+import           Paths_supernova                ( version )+import           Proto.PulsarApi+import qualified Proto.PulsarApi_Fields        as F+import           Pulsar.Types++connect :: BaseCommand+connect = defMessage+    & F.connect .~ conn+ where+  conn :: CommandConnect+  conn = defMessage+    & F.clientVersion .~ "Pulsar-Client-Haskell-v" <> T.pack (showVersion version)+    & F.protocolVersion .~ 15++subscribe :: B.Word64 -> B.Word64 -> Topic -> SubscriptionName -> BaseCommand+subscribe req cid topic (SubscriptionName sub) = defMessage+    & F.type' .~ BaseCommand'SUBSCRIBE+    & F.subscribe .~ subs+ where+  subs :: CommandSubscribe+  subs = defMessage+    & F.topic .~ T.pack (show topic)+    & F.subscription .~ sub+    & F.subType .~ CommandSubscribe'Shared+    & F.consumerId .~ cid+    & F.requestId .~ req++flow :: B.Word64 -> BaseCommand+flow cid = defMessage+    & F.type' .~ BaseCommand'FLOW+    & F.flow .~ flowCmd+ where+  flowCmd :: CommandFlow+  flowCmd = defMessage+    & F.messagePermits .~ 100+    & F.consumerId .~ cid++ack :: B.Word64 -> MessageIdData -> BaseCommand+ack cid msgId = defMessage+    & F.type' .~ BaseCommand'ACK+    & F.ack .~ ackCmd+ where+  ackCmd :: CommandAck+  ackCmd = defMessage+    & F.messageId .~ [ msgId ]+    & F.consumerId .~ cid++closeConsumer :: B.Word64 -> B.Word64 -> BaseCommand+closeConsumer req cid = defMessage+    & F.type' .~ BaseCommand'CLOSE_CONSUMER+    & F.closeConsumer .~ close+ where+  close :: CommandCloseConsumer+  close = defMessage+    & F.consumerId .~ cid+    & F.requestId .~ req++producer :: B.Word64 -> B.Word64 -> Topic -> BaseCommand+producer req pid topic = defMessage+    & F.type' .~ BaseCommand'PRODUCER+    & F.producer .~ prod+ where+  prod :: CommandProducer+  prod = defMessage+    & F.topic .~ T.pack (show topic)+    & F.producerId .~ pid+    & F.requestId .~ req++closeProducer :: B.Word64 -> B.Word64 -> BaseCommand+closeProducer req pid = defMessage+    & F.type' .~ BaseCommand'CLOSE_PRODUCER+    & F.closeProducer .~ prod+ where+  prod :: CommandCloseProducer+  prod = defMessage+    & F.producerId .~ pid+    & F.requestId .~ req++send :: B.Word64 -> B.Word64 -> BaseCommand+send pid sid = defMessage+    & F.type' .~ BaseCommand'SEND+    & F.send .~ sendCmd+ where+  sendCmd :: CommandSend+  sendCmd = defMessage+    & F.producerId .~ pid+    & F.sequenceId .~ sid++lookup :: B.Word64 -> Topic -> BaseCommand+lookup req topic = defMessage+    & F.type' .~ BaseCommand'LOOKUP+    & F.lookupTopic .~ lut+ where+  lut :: CommandLookupTopic+  lut = defMessage+    & F.topic .~ T.pack (show topic)+    & F.requestId .~ req++------- Keep Alive --------++ping :: BaseCommand+ping = defMessage+    & F.type' .~ BaseCommand'PING+    & F.ping .~ defMessage++pong :: BaseCommand+pong = defMessage+    & F.type' .~ BaseCommand'PONG+    & F.pong .~ defMessage++------- Metadata for Payload commands --------++messageMetadata :: MessageMetadata+messageMetadata = defMessage
+ src/Pulsar/Protocol/Decoder.hs view
@@ -0,0 +1,84 @@+{-# LANGUAGE FlexibleInstances, LambdaCase, OverloadedStrings #-}++{- A decoder that understands the Pulsar protocol, as specified at: http://pulsar.apache.org/docs/en/develop-binary-protocol -}+module Pulsar.Protocol.Decoder+  ( decodeBaseCommand+  , dropPayloadGarbage+  )+where++import           Control.Monad                  ( unless )+import qualified Data.Binary.Get               as B+import qualified Data.Binary.Put               as B+import qualified Data.ByteString.Lazy.Char8    as CL+import           Data.Digest.CRC32C             ( crc32c )+import           Data.Bifunctor                 ( bimap )+import           Data.Int                       ( Int32 )+import qualified Data.ProtoLens.Encoding       as PL+import           Pulsar.Protocol.Frame++{-+ - These 5 bytes are part of a total of 8 bytes sent as the payload's prefix from the Java client.+ - Apparently that's how Google's FlatBuffers serialize data: https://google.github.io/flatbuffers/+ -+ - Source: https://github.com/apache/pulsar/blob/master/pulsar-io/kinesis/src/main/java/org/apache/pulsar/io/kinesis/fbs/Message.java#L22+ -+ - More info on the Ascii spec: https://www.december.com/html/spec/ascii.html. Maybe this could be helpful: https://hackage.haskell.org/package/flatbuffers+ -}+dropPayloadGarbage :: CL.ByteString -> CL.ByteString+dropPayloadGarbage bs =+  maybe bs (CL.drop 3) (CL.stripPrefix "\NUL\NUL\NUL\EOT\CAN" bs)++parseFrame :: B.Get Frame+parseFrame = do+  ts <- B.getInt32be+  cs <- B.getInt32be+  ms <- B.getLazyByteString (fromIntegral cs)+  let simpleCmd = SimpleCommand ts cs ms+  B.isEmpty >>= \case+    True  -> return $ SimpleFrame simpleCmd+    False -> parsePayload ts cs simpleCmd++validateCheckSum :: Frame -> B.Get Frame+validateCheckSum (PayloadFrame sc (PayloadCommand cs ms md pl)) =+  let+    metaSize = CL.toStrict (B.runPut $ B.putInt32be ms)+    metadata = CL.toStrict md+    payload  = CL.toStrict pl+    checksum = crc32c $ metaSize <> metadata <> payload+    frame    = PayloadFrame sc (PayloadCommand cs ms md (dropPayloadGarbage pl))+  in+    if checksum == cs then return $! frame else fail "Invalid checksum"+validateCheckSum x = return $! x++parsePayload :: Int32 -> Int32 -> SimpleCmd -> B.Get Frame+parsePayload ts cs simpleCmd = do+  mn <- B.getWord16be+  unless (mn == frameMagicNumber) $ fail ("Invalid magic number: " <> show mn)+  cm <- B.getWord32be+  ms <- B.getInt32be+  md <- B.getLazyByteString . fromIntegral $ ms+  -- 14 remaining bytes = 4 (command size field) + 2 (magic number) + 4 (checksum) + 4 (metadata size field)+  pl <- payload $ ts - (14 + cs + ms)+  let payloadCmd = PayloadCommand cm ms md pl+  validateCheckSum (PayloadFrame simpleCmd payloadCmd)+ where+  payload rms | rms > 0   = B.getLazyByteString $ fromIntegral rms+              | otherwise = pure CL.empty++decodeFrame :: CL.ByteString -> Either String Frame+decodeFrame =+  bimap (\(_, _, e) -> e) (\(_, _, f) -> f) . B.runGetOrFail parseFrame++decodeBaseCommand :: CL.ByteString -> Either String Response+decodeBaseCommand bytes = decodeFrame bytes >>= \case+  SimpleFrame s -> do+    cmd <- PL.decodeMessage (CL.toStrict $ frameMessage s)+    return $ SimpleResponse cmd+  PayloadFrame s (PayloadCommand _ _ md pl) -> do+    cmd  <- PL.decodeMessage . CL.toStrict $ frameMessage s+    meta <- PL.decodeMessage . CL.toStrict $ md+    return $ PayloadResponse cmd meta (payload pl)+   where+    payload p | CL.null p = Nothing+              | otherwise = Just $ Payload p
+ src/Pulsar/Protocol/Encoder.hs view
@@ -0,0 +1,71 @@+{-# LANGUAGE OverloadedStrings #-}++{- An encoder that understands the Pulsar protocol, as specified at: http://pulsar.apache.org/docs/en/develop-binary-protocol -}+module Pulsar.Protocol.Encoder+  ( encodeBaseCommand+  )+where++import qualified Data.Binary.Put               as B+import qualified Data.ByteString.Lazy.Char8    as CL+import           Data.Digest.CRC32C             ( crc32c )+import           Data.Int                       ( Int32 )+import           Data.Maybe                     ( fromMaybe )+import qualified Data.ProtoLens.Encoding       as PL+import           Proto.PulsarApi                ( BaseCommand+                                                , MessageMetadata+                                                )+import           Pulsar.Protocol.Frame++mkSimpleCommand :: Int32 -> BaseCommand -> SimpleCmd+mkSimpleCommand extraBytes cmd = SimpleCommand+  { frameTotalSize   = cmdSize + extraBytes+  , frameCommandSize = cmdSize+  , frameMessage     = msg+  }+ where+  msg     = CL.fromStrict $ PL.encodeMessage cmd+  cmdSize = fromIntegral $ CL.length msg++mkPayloadCommand+  :: BaseCommand -> MessageMetadata -> Payload -> (SimpleCmd, PayloadCmd)+mkPayloadCommand cmd meta (Payload pl) = (simpleCmd, payloadCmd)+ where+  -- payload fields+  metadata    = PL.encodeMessage meta+  metaSize    = fromIntegral . CL.length . CL.fromStrict $ metadata+  metaSizeBS  = B.runPut . B.putInt32be $ metaSize+  checksum    = crc32c $ CL.toStrict metaSizeBS <> metadata <> CL.toStrict pl+  payloadSize = fromIntegral . CL.length $ pl+  -- frame: extra 14 bytes = 2 (magic number) + 4 (checksum) + 4 (metadata size) + 4 (command size)+  extraBytes  = fromIntegral (14 + metaSize) + payloadSize+  simpleCmd   = mkSimpleCommand extraBytes cmd+  payloadCmd  = PayloadCommand { frameCheckSum     = checksum+                               , frameMetadataSize = metaSize+                               , frameMetadata     = CL.fromStrict metadata+                               , framePayload      = pl+                               }++encodeSimpleCmd :: SimpleCmd -> CL.ByteString+encodeSimpleCmd (SimpleCommand ts cs msg) =+  let totalSize   = B.runPut $ B.putInt32be ts+      commandSize = B.runPut $ B.putInt32be cs+  in  totalSize <> commandSize <> msg++encodeFrame :: Frame -> CL.ByteString+encodeFrame (SimpleFrame scmd) = encodeSimpleCmd scmd+encodeFrame (PayloadFrame scmd (PayloadCommand cs mds md p)) =+  let simpleCmd   = encodeSimpleCmd scmd+      metaSizeBS  = B.runPut . B.putInt32be $ mds+      magicNumber = B.runPut . B.putWord16be $ frameMagicNumber+      crc32cSum   = B.runPut . B.putWord32be $ cs+      payloadCmd  = magicNumber <> crc32cSum <> metaSizeBS <> md <> p+  in  simpleCmd <> payloadCmd++encodeBaseCommand+  :: Maybe MessageMetadata -> Maybe Payload -> BaseCommand -> CL.ByteString+encodeBaseCommand (Just meta) p cmd =+  let pl = fromMaybe (Payload "") p+  in  encodeFrame . uncurry PayloadFrame $ mkPayloadCommand cmd meta pl+encodeBaseCommand Nothing _ cmd =+  encodeFrame . SimpleFrame . mkSimpleCommand 4 $ cmd
+ src/Pulsar/Protocol/Frame.hs view
@@ -0,0 +1,43 @@+{- An internal representation of a frame, as specified by the Pulsar protocol: http://pulsar.apache.org/docs/en/develop-binary-protocol -}+module Pulsar.Protocol.Frame where++import qualified Data.Binary                   as B+import qualified Data.ByteString.Lazy.Char8    as CL+import           Data.Int                       ( Int32 )+import           Proto.PulsarApi                ( BaseCommand+                                                , MessageMetadata+                                                )++-- The maximum allowable size of a single frame is 5 MB: http://pulsar.apache.org/docs/en/develop-binary-protocol/#framing+frameMaxSize :: Int+frameMaxSize = 5 * 1024 * 1024 -- 5mb++-- A 2-byte byte array (0x0e01) identifying the current format+frameMagicNumber :: B.Word16+frameMagicNumber  = 0x0e01++data Frame = SimpleFrame SimpleCmd | PayloadFrame SimpleCmd PayloadCmd++-- Simple command: http://pulsar.apache.org/docs/en/develop-binary-protocol/#simple-commands+data SimpleCmd = SimpleCommand+  { frameTotalSize :: Int32        -- The size of the frame, counting everything that comes after it (4 bytes)+  , frameCommandSize :: Int32      -- The size of the protobuf-serialized command (4 bytes)+  , frameMessage :: CL.ByteString  -- The protobuf message serialized in a raw binary format (rather than in protobuf format)+  }++-- Payload command: http://pulsar.apache.org/docs/en/develop-binary-protocol/#payload-commands+data PayloadCmd = PayloadCommand+  { frameCheckSum :: B.Word32       -- A CRC32-C checksum of everything that comes after it (4 bytes)+  , frameMetadataSize :: Int32      -- The size of the message metadata (4 bytes)+  , frameMetadata :: CL.ByteString  -- The message metadata stored as a binary protobuf message+  , framePayload :: CL.ByteString   -- Anything left in the frame is considered the payload and can include any sequence of bytes+  }++newtype Payload = Payload CL.ByteString deriving Show++data Response = SimpleResponse BaseCommand | PayloadResponse BaseCommand MessageMetadata (Maybe Payload) deriving Show++getCommand :: Response -> BaseCommand+getCommand response = case response of+  (SimpleResponse cmd     ) -> cmd+  (PayloadResponse cmd _ _) -> cmd
+ src/Pulsar/Types.hs view
@@ -0,0 +1,79 @@+{-|+Module      : Pulsar.Types+Description : End-user Pulsar API types.+License     : Apache-2.0+Maintainer  : gabriel.volpe@chatroulette.com+Stability   : experimental++End-user types to configure consumers and producers.+-}+module Pulsar.Types where++import qualified Data.ByteString.Lazy.Char8    as CL+import           Data.Char                      ( toLower )+import           Data.String+import qualified Data.Text                     as T+import           Proto.PulsarApi                ( MessageIdData )++{- | A Topic is in the form "type:\/\/tenant\/namespace\/topic-name", which is what the 'Show' instance does. -}+data Topic = Topic+  { type' :: TopicType+  , tenant :: Tenant+  , namespace :: NameSpace+  , name :: TopicName+  }++{- | A default 'Topic': "non-persistent:\/\/public\/default\/my-topic". -}+defaultTopic :: String -> Topic+defaultTopic n = Topic { type'     = NonPersistent+                       , tenant    = Tenant "public"+                       , namespace = NameSpace "default"+                       , name      = TopicName n+                       }++instance Show Topic where+  show (Topic typ tn ns n) =+    toLower <$> show typ <> "://" <> show tn <> "/" <> show ns <> "/" <> show n++{- | A topic can be either 'Persistent' or 'NonPersistent'. -}+data TopicType = Persistent | NonPersistent++instance Show TopicType where+  show Persistent    = "persistent"+  show NonPersistent = "non-persistent"++{- | A tenant can be any string value. Default value is "public". -}+newtype Tenant = Tenant String++instance Show Tenant where+  show (Tenant t) = t++{- | A namespace can be any string value. Default value is "default". -}+newtype NameSpace = NameSpace String++instance Show NameSpace where+  show (NameSpace t) = t++{- | A topic name can be any string value. -}+newtype TopicName = TopicName String++instance Show TopicName where+  show (TopicName t) = t++{- | A message id, needed for acknowledging messages. See 'Pulsar.Consumer.ack'. -}+newtype MsgId = MsgId MessageIdData++{- | A consumed message, containing both 'MsgId' and payload as bytestring. -}+data Message = Message MsgId CL.ByteString++{- | A produced message, containing just a payload as bytestring. -}+newtype PulsarMessage = PulsarMessage CL.ByteString deriving Show++instance IsString PulsarMessage where+  fromString = PulsarMessage . CL.pack++{- | A subscription name can be any string value. -}+newtype SubscriptionName = SubscriptionName T.Text deriving Show++instance IsString SubscriptionName where+  fromString = SubscriptionName . T.pack
+ src/pulsar_api.proto view
@@ -0,0 +1,963 @@+/**+ * Licensed to the Apache Software Foundation (ASF) under one+ * or more contributor license agreements.  See the NOTICE file+ * distributed with this work for additional information+ * regarding copyright ownership.  The ASF licenses this file+ * to you 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.+ */++// Source: https://raw.githubusercontent.com/apache/pulsar/master/pulsar-common/src/main/proto/PulsarApi.proto+syntax = "proto2";++package pulsar.proto;+option java_package = "org.apache.pulsar.common.api.proto";+option optimize_for = LITE_RUNTIME;++message Schema {+    enum Type {+        None = 0;+        String = 1;+        Json = 2;+        Protobuf = 3;+        Avro = 4;+        Bool = 5;+        Int8 = 6;+        Int16 = 7;+        Int32 = 8;+        Int64 = 9;+        Float = 10;+        Double = 11;+        Date = 12;+        Time = 13;+        Timestamp = 14;+        KeyValue = 15;+    }++    required string name = 1;+    required bytes schema_data = 3;+    required Type type = 4;+    repeated KeyValue properties = 5;++}++message MessageIdData {+    required uint64 ledgerId = 1;+    required uint64 entryId  = 2;+    optional int32 partition = 3 [default = -1];+    optional int32 batch_index = 4 [default = -1];+    repeated int64 ack_set = 5;+}++message KeyValue {+    required string key = 1;+    required string value = 2;+}++message KeyLongValue {+    required string key = 1;+    required uint64 value = 2;+}++message IntRange {+    required int32 start = 1;+    required int32 end = 2;+}++message EncryptionKeys {+    required string key = 1;+    required bytes value = 2;+    repeated KeyValue metadata = 3;+}++enum CompressionType {+    NONE   = 0;+    LZ4    = 1;+    ZLIB   = 2;+    ZSTD   = 3;+    SNAPPY   = 4;+}++message MessageMetadata {+    required string producer_name   = 1;+    required uint64 sequence_id     = 2;+    required uint64 publish_time    = 3;+    repeated KeyValue properties    = 4;++    // Property set on replicated message,+    // includes the source cluster name+    optional string replicated_from = 5;+    //key to decide partition for the msg+    optional string partition_key   = 6;+    // Override namespace's replication+    repeated string replicate_to    = 7;+    optional CompressionType compression = 8 [default = NONE];+    optional uint32 uncompressed_size = 9 [default = 0];+    // Removed below checksum field from Metadata as+    // it should be part of send-command which keeps checksum of header + payload+    //optional sfixed64 checksum = 10;+    // differentiate single and batch message metadata+    optional int32 num_messages_in_batch = 11 [default = 1];++    // the timestamp that this event occurs. it is typically set by applications.+    // if this field is omitted, `publish_time` can be used for the purpose of `event_time`.+    optional uint64 event_time = 12 [default = 0];+    // Contains encryption key name, encrypted key and metadata to describe the key+    repeated EncryptionKeys encryption_keys = 13;+    // Algorithm used to encrypt data key+    optional string encryption_algo = 14;+    // Additional parameters required by encryption+    optional bytes encryption_param = 15;+    optional bytes schema_version = 16;++    optional bool partition_key_b64_encoded = 17 [ default = false ];+    // Specific a key to overwrite the message key which used for ordering dispatch in Key_Shared mode.+    optional bytes ordering_key = 18;++    // Mark the message to be delivered at or after the specified timestamp+    optional int64 deliver_at_time = 19;++    // Identify whether a message is a "marker" message used for+    // internal metadata instead of application published data.+    // Markers will generally not be propagated back to clients+    optional int32 marker_type = 20;++    // transaction related message info+    optional uint64 txnid_least_bits = 22;+    optional uint64 txnid_most_bits = 23;++    /// Add highest sequence id to support batch message with external sequence id+    optional uint64 highest_sequence_id = 24 [default = 0];++    // Indicate if the message payload value is set+    optional bool null_value = 25 [ default = false ];+    optional string uuid = 26;+	optional int32 num_chunks_from_msg = 27;+	optional int32 total_chunk_msg_size = 28;+	optional int32 chunk_id = 29;++    // Indicate if the message partition key is set+    optional bool null_partition_key = 30 [default = false];+}++message SingleMessageMetadata {+    repeated KeyValue properties    = 1;+    optional string partition_key   = 2;+    required int32 payload_size    = 3;+    optional bool compacted_out     = 4 [default = false];++    // the timestamp that this event occurs. it is typically set by applications.+    // if this field is omitted, `publish_time` can be used for the purpose of `event_time`.+    optional uint64 event_time = 5 [default = 0];+    optional bool partition_key_b64_encoded = 6 [ default = false ];+    // Specific a key to overwrite the message key which used for ordering dispatch in Key_Shared mode.+    optional bytes ordering_key = 7;+    // Allows consumer retrieve the sequence id that the producer set.+    optional uint64 sequence_id = 8;+    // Indicate if the message payload value is set+    optional bool null_value = 9 [ default = false ];+    // Indicate if the message partition key is set+    optional bool null_partition_key = 10 [ default = false];+}++enum ServerError {+    UnknownError        = 0;+    MetadataError       = 1; // Error with ZK/metadata+    PersistenceError    = 2; // Error writing reading from BK+    AuthenticationError = 3; // Non valid authentication+    AuthorizationError  = 4; // Not authorized to use resource++    ConsumerBusy        = 5; // Unable to subscribe/unsubscribe because+                             // other consumers are connected+    ServiceNotReady     = 6; // Any error that requires client retry operation with a fresh lookup+    ProducerBlockedQuotaExceededError = 7; // Unable to create producer because backlog quota exceeded+    ProducerBlockedQuotaExceededException = 8; // Exception while creating producer because quota exceeded+    ChecksumError = 9; // Error while verifying message checksum+    UnsupportedVersionError = 10; // Error when an older client/version doesn't support a required feature+    TopicNotFound = 11; // Topic not found+    SubscriptionNotFound = 12; // Subscription not found+    ConsumerNotFound = 13; // Consumer not found+    TooManyRequests = 14; // Error with too many simultaneously request+    TopicTerminatedError = 15; // The topic has been terminated++    ProducerBusy         = 16; // Producer with same name is already connected+    InvalidTopicName = 17; // The topic name is not valid++    IncompatibleSchema = 18; // Specified schema was incompatible with topic schema+    ConsumerAssignError = 19; // Dispatcher assign consumer error++    TransactionCoordinatorNotFound = 20; // Transaction coordinator not found error+    InvalidTxnStatus = 21; // Invalid txn status error+    NotAllowedError = 22; // Not allowed error+}++enum AuthMethod {+    AuthMethodNone   = 0;+    AuthMethodYcaV1  = 1;+    AuthMethodAthens = 2;+}++// Each protocol version identify new features that are+// incrementally added to the protocol+enum ProtocolVersion {+    v0 = 0;  // Initial versioning+    v1 = 1;  // Added application keep-alive+    v2 = 2;  // Added RedeliverUnacknowledgedMessages Command+    v3 = 3;  // Added compression with LZ4 and ZLib+    v4 = 4;  // Added batch message support+    v5 = 5;  // Added disconnect client w/o closing connection+    v6 = 6;  // Added checksum computation for metadata + payload+    v7 = 7;  // Added CommandLookupTopic - Binary Lookup+    v8 = 8;  // Added CommandConsumerStats - Client fetches broker side consumer stats+    v9 = 9;  // Added end of topic notification+    v10 = 10;// Added proxy to broker+    v11 = 11;// C++ consumers before this version are not correctly handling the checksum field+    v12 = 12;// Added get topic's last messageId from broker+             // Added CommandActiveConsumerChange+             // Added CommandGetTopicsOfNamespace+    v13 = 13; // Schema-registry : added avro schema format for json+    v14 = 14; // Add CommandAuthChallenge and CommandAuthResponse for mutual auth+              // Added Key_Shared subscription+    v15 = 15; // Add CommandGetOrCreateSchema and CommandGetOrCreateSchemaResponse+}++message CommandConnect {+    required string client_version = 1;+    optional AuthMethod auth_method = 2; // Deprecated. Use "auth_method_name" instead.+    optional string auth_method_name = 5;+    optional bytes auth_data = 3;+    optional int32 protocol_version = 4 [default = 0];++    // Client can ask to be proxyied to a specific broker+    // This is only honored by a Pulsar proxy+    optional string proxy_to_broker_url = 6;++    // Original principal that was verified by+    // a Pulsar proxy. In this case the auth info above+    // will be the auth of the proxy itself+    optional string original_principal = 7;++    // Original auth role and auth Method that was passed+    // to the proxy. In this case the auth info above+    // will be the auth of the proxy itself+    optional string original_auth_data = 8;+    optional string original_auth_method = 9;++    // Feature flags+    optional FeatureFlags feature_flags = 10;+}++message FeatureFlags {+  optional bool supports_auth_refresh = 1 [default = false];+}++message CommandConnected {+    required string server_version = 1;+    optional int32 protocol_version = 2 [default = 0];+    optional int32 max_message_size = 3;+}++message CommandAuthResponse {+    optional string client_version = 1;+    optional AuthData response = 2;+    optional int32 protocol_version = 3 [default = 0];+}++message CommandAuthChallenge {+    optional string server_version = 1;+    optional AuthData challenge = 2;+    optional int32 protocol_version = 3 [default = 0];+}++// To support mutual authentication type, such as Sasl, reuse this command to mutual auth.+message AuthData {+    optional string auth_method_name = 1;+    optional bytes auth_data = 2;+}++enum KeySharedMode {+    AUTO_SPLIT = 0;+    STICKY = 1;+}++message KeySharedMeta {+    required KeySharedMode keySharedMode = 1;+    repeated IntRange hashRanges = 3;+    optional bool allowOutOfOrderDelivery = 4 [default = false];+}++message CommandSubscribe {+    enum SubType {+        Exclusive = 0;+        Shared    = 1;+        Failover  = 2;+        Key_Shared = 3;+    }+    required string topic        = 1;+    required string subscription = 2;+    required SubType subType     = 3;++    required uint64 consumer_id  = 4;+    required uint64 request_id   = 5;+    optional string consumer_name = 6;+    optional int32 priority_level = 7;++    // Signal wether the subscription should be backed by a+    // durable cursor or not+    optional bool durable = 8 [default = true];++    // If specified, the subscription will position the cursor+    // markd-delete position  on the particular message id and+    // will send messages from that point+    optional MessageIdData start_message_id = 9;++    /// Add optional metadata key=value to this consumer+    repeated KeyValue metadata = 10;++    optional bool read_compacted = 11;++    optional Schema schema = 12;+    enum InitialPosition {+        Latest   = 0;+        Earliest = 1;+    }+    // Signal whether the subscription will initialize on latest+    // or not -- earliest+    optional InitialPosition initialPosition = 13 [default = Latest];++    // Mark the subscription as "replicated". Pulsar will make sure+    // to periodically sync the state of replicated subscriptions+    // across different clusters (when using geo-replication).+    optional bool replicate_subscription_state = 14;++    // If true, the subscribe operation will cause a topic to be+    // created if it does not exist already (and if topic auto-creation+    // is allowed by broker.+    // If false, the subscribe operation will fail if the topic+    // does not exist.+    optional bool force_topic_creation = 15 [default = true];++    // If specified, the subscription will reset cursor's position back+    // to specified seconds and  will send messages from that point+    optional uint64 start_message_rollback_duration_sec = 16 [default = 0];++    optional KeySharedMeta keySharedMeta = 17;+}++message CommandPartitionedTopicMetadata {+    required string topic            = 1;+    required uint64 request_id       = 2;+    // TODO - Remove original_principal, original_auth_data, original_auth_method+    // Original principal that was verified by+    // a Pulsar proxy.+    optional string original_principal = 3;++    // Original auth role and auth Method that was passed+    // to the proxy.+    optional string original_auth_data = 4;+    optional string original_auth_method = 5;+}++message CommandPartitionedTopicMetadataResponse {+    enum LookupType {+        Success  = 0;+        Failed   = 1;+    }+    optional uint32 partitions            = 1;  // Optional in case of error+    required uint64 request_id            = 2;+    optional LookupType response          = 3;+    optional ServerError error            = 4;+    optional string message               = 5;+}++message CommandLookupTopic {+    required string topic            = 1;+    required uint64 request_id       = 2;+    optional bool authoritative      = 3 [default = false];++    // TODO - Remove original_principal, original_auth_data, original_auth_method+    // Original principal that was verified by+    // a Pulsar proxy.+    optional string original_principal = 4;++    // Original auth role and auth Method that was passed+    // to the proxy.+    optional string original_auth_data = 5;+    optional string original_auth_method = 6;+    //+    optional string advertised_listener_name = 7;+}++message CommandLookupTopicResponse {+    enum LookupType {+        Redirect = 0;+        Connect  = 1;+        Failed   = 2;+    }++    optional string brokerServiceUrl      = 1; // Optional in case of error+    optional string brokerServiceUrlTls   = 2;+    optional LookupType response          = 3;+    required uint64 request_id            = 4;+    optional bool authoritative           = 5 [default = false];+    optional ServerError error            = 6;+    optional string message               = 7;++    // If it's true, indicates to the client that it must+    // always connect through the service url after the+    // lookup has been completed.+    optional bool proxy_through_service_url = 8 [default = false];+}++/// Create a new Producer on a topic, assigning the given producer_id,+/// all messages sent with this producer_id will be persisted on the topic+message CommandProducer {+    required string topic         = 1;+    required uint64 producer_id   = 2;+    required uint64 request_id    = 3;++    /// If a producer name is specified, the name will be used,+    /// otherwise the broker will generate a unique name+    optional string producer_name = 4;++    optional bool encrypted       = 5 [default = false];++    /// Add optional metadata key=value to this producer+    repeated KeyValue metadata    = 6;++    optional Schema schema = 7;++    // If producer reconnect to broker, the epoch of this producer will +1+    optional uint64 epoch = 8 [default = 0];++    // Indicate the name of the producer is generated or user provided+    // Use default true here is in order to be forward compatible with the client+    optional bool user_provided_producer_name = 9 [default = true];+}++message CommandSend {+    required uint64 producer_id = 1;+    required uint64 sequence_id = 2;+    optional int32 num_messages = 3 [default = 1];+    optional uint64 txnid_least_bits = 4 [default = 0];+    optional uint64 txnid_most_bits = 5 [default = 0];++    /// Add highest sequence id to support batch message with external sequence id+    optional uint64 highest_sequence_id = 6 [default = 0];+    optional bool is_chunk     =7 [default = false];+}++message CommandSendReceipt {+    required uint64 producer_id = 1;+    required uint64 sequence_id = 2;+    optional MessageIdData message_id = 3;+    optional uint64 highest_sequence_id = 4 [default = 0];+}++message CommandSendError {+    required uint64 producer_id = 1;+    required uint64 sequence_id = 2;+    required ServerError error  = 3;+    required string message     = 4;+}++message CommandMessage {+    required uint64 consumer_id       = 1;+    required MessageIdData message_id = 2;+    optional uint32 redelivery_count  = 3 [default = 0];+    repeated int64 ack_set = 4;+}++message CommandAck {+    enum AckType {+        Individual = 0;+        Cumulative = 1;+    }++    required uint64 consumer_id       = 1;+    required AckType ack_type         = 2;++    // In case of individual acks, the client can pass a list of message ids+    repeated MessageIdData message_id = 3;++    // Acks can contain a flag to indicate the consumer+    // received an invalid message that got discarded+    // before being passed on to the application.+    enum ValidationError {+        UncompressedSizeCorruption = 0;+        DecompressionError = 1;+        ChecksumMismatch = 2;+        BatchDeSerializeError = 3;+        DecryptionError = 4;+    }++    optional ValidationError validation_error = 4;+    repeated KeyLongValue properties = 5;++    optional uint64 txnid_least_bits = 6 [default = 0];+    optional uint64 txnid_most_bits = 7 [default = 0];+}++message CommandAckResponse {+    required uint64 consumer_id = 1;+    optional uint64 txnid_least_bits  = 2 [default = 0];+    optional uint64 txnid_most_bits = 3 [default = 0];+    optional ServerError error = 4;+    optional string message = 5;+}++// changes on active consumer+message CommandActiveConsumerChange {+        required uint64 consumer_id    = 1;+        optional bool is_active     = 2 [default = false];+}++message CommandFlow {+    required uint64 consumer_id       = 1;++    // Max number of messages to prefetch, in addition+    // of any number previously specified+    required uint32 messagePermits     = 2;+}++message CommandUnsubscribe {+    required uint64 consumer_id = 1;+    required uint64 request_id  = 2;+}++// Reset an existing consumer to a particular message id+message CommandSeek {+    required uint64 consumer_id = 1;+    required uint64 request_id  = 2;++    optional MessageIdData message_id = 3;+    optional uint64 message_publish_time = 4;+}++// Message sent by broker to client when a topic+// has been forcefully terminated and there are no more+// messages left to consume+message CommandReachedEndOfTopic {+    required uint64 consumer_id = 1;+}++message CommandCloseProducer {+    required uint64 producer_id = 1;+    required uint64 request_id = 2;+}++message CommandCloseConsumer {+    required uint64 consumer_id = 1;+    required uint64 request_id = 2;+}++message CommandRedeliverUnacknowledgedMessages {+    required uint64 consumer_id = 1;+    repeated MessageIdData message_ids = 2;+}++message CommandSuccess {+    required uint64 request_id = 1;+    optional Schema schema = 2;+}++/// Response from CommandProducer+message CommandProducerSuccess {+    required uint64 request_id    = 1;+    required string producer_name = 2;++    // The last sequence id that was stored by this producer in the previous session+    // This will only be meaningful if deduplication has been enabled.+    optional int64  last_sequence_id = 3 [default = -1];+    optional bytes schema_version = 4;+}++message CommandError {+    required uint64 request_id  = 1;+    required ServerError error = 2;+    required string message    = 3;+}++// Commands to probe the state of connection.+// When either client or broker doesn't receive commands for certain+// amount of time, they will send a Ping probe.+message CommandPing {+}+message CommandPong {+}++message CommandConsumerStats {+        required uint64 request_id         = 1;+        // required string topic_name         = 2;+        // required string subscription_name  = 3;+        required uint64 consumer_id        = 4;+}++message CommandConsumerStatsResponse {+        required uint64 request_id              = 1;+        optional ServerError error_code         = 2;+        optional string error_message           = 3;++        /// Total rate of messages delivered to the consumer. msg/s+        optional double msgRateOut                  = 4;++        /// Total throughput delivered to the consumer. bytes/s+        optional double msgThroughputOut            = 5;++        /// Total rate of messages redelivered by this consumer. msg/s+        optional double msgRateRedeliver            = 6;++        /// Name of the consumer+        optional string consumerName                = 7;++        /// Number of available message permits for the consumer+        optional uint64 availablePermits            = 8;++        /// Number of unacknowledged messages for the consumer+        optional uint64 unackedMessages             = 9;++        /// Flag to verify if consumer is blocked due to reaching threshold of unacked messages+        optional bool blockedConsumerOnUnackedMsgs  = 10;++        /// Address of this consumer+        optional string address                     = 11;++        /// Timestamp of connection+        optional string connectedSince              = 12;++        /// Whether this subscription is Exclusive or Shared or Failover+        optional string type                        = 13;++        /// Total rate of messages expired on this subscription. msg/s+        optional double msgRateExpired              = 14;++        /// Number of messages in the subscription backlog+        optional uint64 msgBacklog                  = 15;+}++message CommandGetLastMessageId {+    required uint64 consumer_id = 1;+    required uint64 request_id  = 2;+}++message CommandGetLastMessageIdResponse {+    required MessageIdData last_message_id = 1;+    required uint64 request_id  = 2;+}++message CommandGetTopicsOfNamespace {+    enum Mode {+        PERSISTENT = 0;+        NON_PERSISTENT = 1;+        ALL = 2;+    }+    required uint64 request_id    = 1;+    required string namespace    = 2;+    optional Mode mode = 3 [default = PERSISTENT];+}++message CommandGetTopicsOfNamespaceResponse {+    required uint64 request_id    = 1;+    repeated string topics         = 2;+}++message CommandGetSchema {+    required uint64 request_id = 1;+    required string topic      = 2;++    optional bytes schema_version = 3;+}++message CommandGetSchemaResponse {+    required uint64 request_id      = 1;+    optional ServerError error_code = 2;+    optional string error_message   = 3;++    optional Schema schema          = 4;+    optional bytes schema_version   = 5;+}++message CommandGetOrCreateSchema {+    required uint64 request_id = 1;+    required string topic      = 2;+    required Schema schema     = 3;+}++message CommandGetOrCreateSchemaResponse {+    required uint64 request_id      = 1;+    optional ServerError error_code = 2;+    optional string error_message   = 3;++    optional bytes schema_version   = 4;+}++/// --- transaction related ---++enum TxnAction {+    COMMIT = 0;+    ABORT = 1;+}++message CommandNewTxn {+    required uint64 request_id = 1;+    optional uint64 txn_ttl_seconds = 2 [default = 0];+    optional uint64 tc_id = 3 [default = 0];+}++message CommandNewTxnResponse {+    required uint64 request_id = 1;+    optional uint64 txnid_least_bits = 2 [default = 0];+    optional uint64 txnid_most_bits = 3 [default = 0];+    optional ServerError error = 4;+    optional string message = 5;+}++message CommandAddPartitionToTxn {+    required uint64 request_id = 1;+    optional uint64 txnid_least_bits = 2 [default = 0];+    optional uint64 txnid_most_bits = 3 [default = 0];+    repeated string partitions = 4;+}++message CommandAddPartitionToTxnResponse {+    required uint64 request_id = 1;+    optional uint64 txnid_least_bits = 2 [default = 0];+    optional uint64 txnid_most_bits = 3 [default = 0];+    optional ServerError error = 4;+    optional string message = 5;+}++message Subscription {+    required string topic = 1;+    required string subscription = 2;+}+message CommandAddSubscriptionToTxn {+    required uint64 request_id = 1;+    optional uint64 txnid_least_bits = 2 [default = 0];+    optional uint64 txnid_most_bits = 3 [default = 0];+    repeated Subscription subscription = 4;+}++message CommandAddSubscriptionToTxnResponse {+    required uint64 request_id = 1;+    optional uint64 txnid_least_bits = 2 [default = 0];+    optional uint64 txnid_most_bits = 3 [default = 0];+    optional ServerError error = 4;+    optional string message = 5;+}++message CommandEndTxn {+    required uint64 request_id = 1;+    optional uint64 txnid_least_bits = 2 [default = 0];+    optional uint64 txnid_most_bits = 3 [default = 0];+    optional TxnAction txn_action = 4;+}++message CommandEndTxnResponse {+    required uint64 request_id = 1;+    optional uint64 txnid_least_bits = 2 [default = 0];+    optional uint64 txnid_most_bits = 3 [default = 0];+    optional ServerError error = 4;+    optional string message = 5;+}++message CommandEndTxnOnPartition {+    required uint64 request_id = 1;+    optional uint64 txnid_least_bits = 2 [default = 0];+    optional uint64 txnid_most_bits = 3 [default = 0];+    optional string topic = 4;+    optional TxnAction txn_action = 5;+}++message CommandEndTxnOnPartitionResponse {+    required uint64 request_id = 1;+    optional uint64 txnid_least_bits = 2 [default = 0];+    optional uint64 txnid_most_bits = 3 [default = 0];+    optional ServerError error = 4;+    optional string message = 5;+}++message CommandEndTxnOnSubscription {+    required uint64 request_id = 1;+    optional uint64 txnid_least_bits = 2 [default = 0];+    optional uint64 txnid_most_bits = 3 [default = 0];+    optional Subscription subscription= 4;+    optional TxnAction txn_action = 5;+}++message CommandEndTxnOnSubscriptionResponse {+    required uint64 request_id = 1;+    optional uint64 txnid_least_bits = 2 [default = 0];+    optional uint64 txnid_most_bits = 3 [default = 0];+    optional ServerError error = 4;+    optional string message = 5;+}++message BaseCommand {+    enum Type {+        CONNECT     = 2;+        CONNECTED   = 3;+        SUBSCRIBE   = 4;++        PRODUCER    = 5;++        SEND        = 6;+        SEND_RECEIPT= 7;+        SEND_ERROR  = 8;++        MESSAGE     = 9;+        ACK         = 10;+        FLOW        = 11;++        UNSUBSCRIBE = 12;++        SUCCESS     = 13;+        ERROR       = 14;++        CLOSE_PRODUCER = 15;+        CLOSE_CONSUMER = 16;++        PRODUCER_SUCCESS = 17;++        PING = 18;+        PONG = 19;++        REDELIVER_UNACKNOWLEDGED_MESSAGES = 20;++        PARTITIONED_METADATA           = 21;+        PARTITIONED_METADATA_RESPONSE  = 22;++        LOOKUP           = 23;+        LOOKUP_RESPONSE  = 24;++        CONSUMER_STATS        = 25;+        CONSUMER_STATS_RESPONSE    = 26;++        REACHED_END_OF_TOPIC = 27;++        SEEK = 28;++        GET_LAST_MESSAGE_ID = 29;+        GET_LAST_MESSAGE_ID_RESPONSE = 30;++        ACTIVE_CONSUMER_CHANGE = 31;+++        GET_TOPICS_OF_NAMESPACE             = 32;+        GET_TOPICS_OF_NAMESPACE_RESPONSE     = 33;++        GET_SCHEMA = 34;+        GET_SCHEMA_RESPONSE = 35;++        AUTH_CHALLENGE = 36;+        AUTH_RESPONSE = 37;++        ACK_RESPONSE = 38;++        GET_OR_CREATE_SCHEMA = 39;+        GET_OR_CREATE_SCHEMA_RESPONSE = 40;++        // transaction related+        NEW_TXN = 50;+        NEW_TXN_RESPONSE = 51;++        ADD_PARTITION_TO_TXN = 52;+        ADD_PARTITION_TO_TXN_RESPONSE = 53;++        ADD_SUBSCRIPTION_TO_TXN = 54;+        ADD_SUBSCRIPTION_TO_TXN_RESPONSE = 55;++        END_TXN = 56;+        END_TXN_RESPONSE = 57;++        END_TXN_ON_PARTITION = 58;+        END_TXN_ON_PARTITION_RESPONSE = 59;++        END_TXN_ON_SUBSCRIPTION = 60;+        END_TXN_ON_SUBSCRIPTION_RESPONSE = 61;++    }+++    required Type type = 1;++    optional CommandConnect connect          = 2;+    optional CommandConnected connected      = 3;++    optional CommandSubscribe subscribe      = 4;+    optional CommandProducer producer        = 5;+    optional CommandSend send                = 6;+    optional CommandSendReceipt send_receipt = 7;+    optional CommandSendError send_error     = 8;+    optional CommandMessage message          = 9;+    optional CommandAck ack                  = 10;+    optional CommandFlow flow                = 11;+    optional CommandUnsubscribe unsubscribe  = 12;++    optional CommandSuccess success          = 13;+    optional CommandError error              = 14;++    optional CommandCloseProducer close_producer = 15;+    optional CommandCloseConsumer close_consumer = 16;++    optional CommandProducerSuccess producer_success = 17;+    optional CommandPing ping = 18;+    optional CommandPong pong = 19;+    optional CommandRedeliverUnacknowledgedMessages redeliverUnacknowledgedMessages = 20;++    optional CommandPartitionedTopicMetadata  partitionMetadata                 = 21;+    optional CommandPartitionedTopicMetadataResponse partitionMetadataResponse  = 22;++    optional CommandLookupTopic lookupTopic                    = 23;+    optional CommandLookupTopicResponse lookupTopicResponse    = 24;++    optional CommandConsumerStats consumerStats                         = 25;+    optional CommandConsumerStatsResponse consumerStatsResponse         = 26;++    optional CommandReachedEndOfTopic reachedEndOfTopic  = 27;++    optional CommandSeek seek = 28;++    optional CommandGetLastMessageId getLastMessageId = 29;+    optional CommandGetLastMessageIdResponse getLastMessageIdResponse = 30;++    optional CommandActiveConsumerChange active_consumer_change = 31;++    optional CommandGetTopicsOfNamespace getTopicsOfNamespace = 32;+    optional CommandGetTopicsOfNamespaceResponse getTopicsOfNamespaceResponse = 33;++    optional CommandGetSchema getSchema = 34;+    optional CommandGetSchemaResponse getSchemaResponse = 35;++    optional CommandAuthChallenge authChallenge = 36;+    optional CommandAuthResponse authResponse = 37;++    optional CommandAckResponse ackResponse = 38;++    optional CommandGetOrCreateSchema getOrCreateSchema = 39;+    optional CommandGetOrCreateSchemaResponse getOrCreateSchemaResponse = 40;++    // transaction related+    optional CommandNewTxn newTxn = 50;+    optional CommandNewTxnResponse newTxnResponse = 51;+    optional CommandAddPartitionToTxn addPartitionToTxn= 52;+    optional CommandAddPartitionToTxnResponse addPartitionToTxnResponse = 53;+    optional CommandAddSubscriptionToTxn addSubscriptionToTxn = 54;+    optional CommandAddSubscriptionToTxnResponse addSubscriptionToTxnResponse = 55;+    optional CommandEndTxn endTxn = 56;+    optional CommandEndTxnResponse endTxnResponse = 57;+    optional CommandEndTxnOnPartition endTxnOnPartition = 58;+    optional CommandEndTxnOnPartitionResponse endTxnOnPartitionResponse = 59;+    optional CommandEndTxnOnSubscription endTxnOnSubscription = 60;+    optional CommandEndTxnOnSubscriptionResponse endTxnOnSubscriptionResponse = 61;+}
+ supernova.cabal view
@@ -0,0 +1,73 @@+cabal-version:       3.0++name:                supernova+version:             0.0.1+synopsis:            Apache Pulsar client for Haskell+description:         Supernova is an Apache Pulsar client that implements the specified TCP protocol.+homepage:            https://github.com/cr-org/supernova+bug-reports:         https://github.com/cr-org/supernova/issues+license:             Apache-2.0+license-file:        LICENSE+author:              Gabriel Volpe+maintainer:          gabriel.volpe@chatroulette.com+category:            Network+build-type:          Custom+extra-source-files:  README.md+                   , src/pulsar_api.proto++custom-setup+  setup-depends:       base+                     , Cabal+                     , proto-lens-setup++library+  exposed-modules:     Pulsar+  other-modules:       Paths_supernova+                     , Proto.PulsarApi+                     , Proto.PulsarApi_Fields+                     , Pulsar.Core+                     , Pulsar.Connection+                     , Pulsar.Consumer+                     , Pulsar.Internal.Core+                     , Pulsar.Internal.Logger+                     , Pulsar.Internal.TCPClient+                     , Pulsar.Producer+                     , Pulsar.Protocol.Commands+                     , Pulsar.Protocol.Decoder+                     , Pulsar.Protocol.Encoder+                     , Pulsar.Protocol.Frame+                     , Pulsar.Types+  autogen-modules:     Paths_supernova+                     , Proto.PulsarApi+                     , Proto.PulsarApi_Fields+  build-depends:       base                  >= 4.13.0 && < 4.14,+                       bifunctor             >= 0.1.0 && < 0.2,+                       binary                >= 0.8.7 && < 0.9,+                       bytestring            >= 0.10.10 && < 0.11,+                       crc32c                >= 0.0.0 && < 0.1,+                       exceptions            >= 0.10.4 && < 0.11,+                       lens-family-core      >= 2.0.0 && < 2.1,+                       logging               >= 3.0.5 && < 3.1,+                       text                  >= 1.2.4 && < 1.3,+                       managed               >= 1.0.7 && < 1.1,+                       network               >= 3.1.2 && < 3.2,+                       proto-lens            >= 0.7.0 && < 0.8,+                       proto-lens-runtime    >= 0.7.0 && < 0.8,+                       unliftio              >= 0.2.13 && < 0.3+  hs-source-dirs:      src+  default-language:    Haskell2010+  ghc-options:         -Wall++test-Suite supernova-tests+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  main-is:             Main.hs+  build-depends:       base+                     , aeson+                     , async+                     , bytestring+                     , supernova+                     , streamly+                     , text+  default-language:     Haskell2010+  ghc-options:         -Wall -threaded -with-rtsopts -N
+ test/Main.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE DeriveAnyClass, DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings, RecordWildCards #-}++module Main where++import           Control.Concurrent             ( threadDelay )+import           Control.Concurrent.Async       ( concurrently_ )+import           Control.Monad                  ( forever )+import           Data.Aeson+import qualified Data.ByteString.Lazy.Char8    as CL+import           Data.Foldable                  ( traverse_ )+import           Data.Text                      ( Text )+import           GHC.Generics                   ( Generic )+import           Pulsar+import           Streamly                       ( asyncly+                                                , maxThreads+                                                )+import qualified Streamly.Prelude              as S++main :: IO ()+main = demo++data Msg = Msg+  { name :: Text+  , amount :: Int+  } deriving (Generic, FromJSON, ToJSON, Show)++messages :: [PulsarMessage]+messages =+  let msg = [Msg "foo" 2, Msg "bar" 5, Msg "taz" 1]+  in  PulsarMessage . encode <$> msg++msgDecoder :: CL.ByteString -> IO ()+msgDecoder bs =+  let msg = decode bs :: Maybe Msg+  in  putStrLn $ "-----------------> " <> show msg++topic :: Topic+topic = defaultTopic "app"++demo :: IO ()+demo = runPulsar resources $ \(Consumer {..}, Producer {..}) ->+  let c = forever $ fetch >>= \(Message i m) -> msgDecoder m >> ack i+      p = forever $ sleep 5 >> traverse_ produce messages+  in  concurrently_ c p++resources :: Pulsar (Consumer IO, Producer IO)+resources = do+  ctx      <- connect defaultConnectData+  consumer <- newConsumer ctx topic "test-sub"+  producer <- newProducer ctx topic+  return (consumer, producer)++sleep :: Int -> IO ()+sleep n = threadDelay (n * 1000000)++logOpts :: LogOptions+logOpts = LogOptions Info StdOut++streamDemo :: IO ()+streamDemo = runPulsar' logOpts resources $ \(Consumer {..}, Producer {..}) ->+  let c = forever $ fetch >>= \(Message i m) -> msgDecoder m >> ack i+      p = forever $ sleep 5 >> traverse_ produce messages+  in  S.drain . asyncly . maxThreads 10 $ S.yieldM c <> S.yieldM p