All Downloads are FREE. Search and download functionalities are using the official Maven repository.

simosiani.skunk-core_2.12.0.3.2.source-code.Channel.scala Maven / Gradle / Ivy

// Copyright (c) 2018-2021 by Rob Norris
// This software is licensed under the MIT License (MIT).
// For more information see LICENSE or https://opensource.org/licenses/MIT

package skunk

import cats.{ Contravariant, Functor, ~> }
import cats.arrow.Profunctor
import cats.effect.Resource
import cats.effect.kernel.MonadCancelThrow
import cats.syntax.all._
import fs2.{ Pipe, Stream }
import skunk.data.{ Identifier, Notification }
import skunk.net.Protocol
import skunk.util.Origin

/**
 * A '''channel''' that can be used for inter-process communication, implemented in terms of
 * `LISTEN` and `NOTIFY`. All instances start life as a `Channel[F, String, Notification]` but can
 * be mapped out to different input and output types. See the linked documentation for more
 * information on the transactional semantics of these operations.
 * @see [[https://www.postgresql.org/docs/10/static/sql-listen.html LISTEN]]
 * @see [[https://www.postgresql.org/docs/10/static/sql-notify.html NOTIFY]]
 * @group Session
 */
trait Channel[F[_], A, B] extends Pipe[F, A, Unit] { outer =>

  /**
   * Construct a `Stream` that subscribes to notifications for this Channel, emits any notifications
   * that arrive (this can happen at any time), then unsubscribes when the stream is terminated.
   * Note that once such a stream is started it is important to consume all notifications as quickly
   * as possible to avoid blocking message processing for other operations on the `Session`
   * (although typically a dedicated `Session` will receive channel notifications so this won't be
   * an issue).
   * @param maxQueued the maximum number of notifications to hold in a queue before [semantically]
   *   blocking message exchange on the controlling `Session`.
   * @group Notifications
   * @see [[https://www.postgresql.org/docs/10/static/sql-listen.html LISTEN]]
   */
  def listen(maxQueued: Int): Stream[F, Notification[B]]

  /** This `Channel` acts as an fs2 `Pipe`. */
  def apply(sa: Stream[F, A]): Stream[F,Unit] =
    sa.evalMap(notify)

  /**
   * Send a notification on the given channel. Note that if the session is in an active transaction
   * the notification will only be sent if the transaction is committed. See the linked
   * documentation for more information.
   * @group Notifications
   * @see [[https://www.postgresql.org/docs/10/static/sql-notify.html NOTIFY]]
   */
  def notify(message: A): F[Unit]

  /**
   * Map notifications to a new type `D`, yielding an `Channel[D, A, D]`.
   * @group Transformations
   */
  def map[D](f: B => D): Channel[F, A, D] =
    dimap(identity[A])(f)

  /**
   * Contramap messages from a new type `C`, yielding an `Channel[D, C, B]`.
   * @group Transformations
   */
  def contramap[C](f: C => A): Channel[F, C, B] =
    dimap(f)(identity[B])

  /**
   * Contramap inputs from a new type `C` and map outputs to a new type `D`, yielding a
   * `Channel[F, C, D]`.
   * @group Transformations
   */
  def dimap[C, D](f: C => A)(g: B => D): Channel[F, C, D] =
    new Channel[F, C, D] {
      def listen(maxQueued: Int): Stream[F, Notification[D]] = outer.listen(maxQueued).map(_.map(g))
      def notify(message: C): F[Unit] = outer.notify(f(message))
    }

  /**
   * Transform this `Channel` by a given `FunctionK`.
   * @group Transformations
   */
  def mapK[G[_]](fk: F ~> G): Channel[G, A, B] =
    new Channel[G, A, B] {
      def listen(maxQueued: Int): Stream[G, Notification[B]] = outer.listen(maxQueued).translate(fk)
      def notify(message: A): G[Unit] = fk(outer.notify(message))
    }

}

/** @group Companions */
object Channel {

  /**
   * Construct a `Channel` given a name and an underlying `Protocol` (note that this is atypical;
   * normally a `Channel` is obtained from a `Session`).
   * @group Constructors
   */
  def fromNameAndProtocol[F[_]: MonadCancelThrow](name: Identifier, proto: Protocol[F]): Channel[F, String, String] =
    new Channel[F, String, String] {

    val listen: F[Unit] =
      proto.execute(Command(s"LISTEN ${name.value}", Origin.unknown, Void.codec)).void

    val unlisten: F[Unit] =
      proto.execute(Command(s"UNLISTEN ${name.value}", Origin.unknown, Void.codec)).void

    def listen(maxQueued: Int): Stream[F, Notification[String]] =
      for {
        _ <- Stream.resource(Resource.make(listen)(_ => unlisten))
        n <- proto.notifications(maxQueued).filter(_.channel === name)
      } yield n

    def notify(message: String): F[Unit] =
      // TODO: escape the message
      proto.execute(Command(s"NOTIFY ${name.value}, '$message'", Origin.unknown, Void.codec)).void

  }

  /**
   * `Channel[F, T, *]` is a covariant functor for all `F` and `T`.
   * @group Typeclass Instances
   */
  implicit def functorChannel[F[_], T]: Functor[Channel[F, T, *]] =
    new Functor[Channel[F, T, *]] {
      def map[A, B](fa: Channel[F, T, A])(f: A => B): Channel[F, T, B] =
        fa.map(f)
    }

  /**
   * `Channel[F, *, T]` is a contravariant functor for all `F` and `T`.
   * @group Typeclass Instances
   */
  implicit def contravariantChannel[F[_], T]: Contravariant[Channel[F, *, T]] =
    new Contravariant[Channel[F, *, T]] {
      def contramap[A, B](fa: Channel[F, A, T])(f: B => A): Channel[F, B, T] =
        fa.contramap(f)
    }

  /**
   * `Channel[F, *, *]` is a profunctor for all `F`.
   * @group Typeclass Instances
   */
  implicit def profunctorChannel[F[_]]: Profunctor[Channel[F, *, *]] =
    new Profunctor[Channel[F, *, *]] {
      def dimap[A, B, C, D](fab: Channel[F, A, B])(f: C => A)(g: B => D): Channel[F, C, D] =
        fab.dimap(f)(g)
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy