├── .gitignore ├── LICENSE ├── README.md ├── build.sbt ├── circle.yml ├── pico-event └── src │ ├── main │ ├── scala │ │ └── org │ │ │ └── pico │ │ │ └── event │ │ │ ├── Bus.scala │ │ │ ├── Cell.scala │ │ │ ├── ClosedSink.scala │ │ │ ├── ClosedSinkSource.scala │ │ │ ├── ClosedSource.scala │ │ │ ├── ClosedSubscribers.scala │ │ │ ├── CompositeSinkSource.scala │ │ │ ├── ComputedView.scala │ │ │ ├── HasForeach.scala │ │ │ ├── IntCell.scala │ │ │ ├── Invalidations.scala │ │ │ ├── LongCell.scala │ │ │ ├── SimpleBus.scala │ │ │ ├── SimpleSinkSource.scala │ │ │ ├── Sink.scala │ │ │ ├── SinkSource.scala │ │ │ ├── Source.scala │ │ │ ├── Subscribers.scala │ │ │ ├── TimedBus.scala │ │ │ ├── View.scala │ │ │ ├── WaitCompleteSink.scala │ │ │ ├── Wrapper.scala │ │ │ ├── concurrent │ │ │ └── ExecutionContextBus.scala │ │ │ ├── io │ │ │ ├── ByteCountOutputStream.scala │ │ │ └── NewlineCountWriter.scala │ │ │ ├── net │ │ │ ├── UdpEmitFailed.scala │ │ │ └── UdpEmitter.scala │ │ │ ├── package.scala │ │ │ ├── std │ │ │ └── all │ │ │ │ └── package.scala │ │ │ └── syntax │ │ │ ├── disposer │ │ │ └── package.scala │ │ │ ├── future │ │ │ └── package.scala │ │ │ ├── hasForeach │ │ │ └── package.scala │ │ │ ├── outputStream │ │ │ └── package.scala │ │ │ ├── sink │ │ │ └── package.scala │ │ │ ├── sinkSource │ │ │ └── package.scala │ │ │ ├── source │ │ │ └── package.scala │ │ │ ├── sourceLike │ │ │ └── package.scala │ │ │ └── writer │ │ │ └── package.scala │ └── tut │ │ └── tutorial.md │ └── test │ └── scala │ └── org │ └── pico │ └── event │ ├── BusSpec.scala │ ├── CellSpec.scala │ ├── ClosedSinkSpec.scala │ ├── ClosedSourceSpec.scala │ ├── SinkSpec.scala │ ├── SourceSpec.scala │ ├── ViewSpec.scala │ ├── concurrent │ └── ExecutionContextBusSpec.scala │ └── performance │ └── ViewSpec.scala ├── pico-fake └── src │ ├── main │ └── scala │ │ └── org │ │ └── pico │ │ └── fake │ │ └── Fake.scala │ └── test │ └── scala │ └── org │ └── pico │ └── fake │ └── FakeSpec.scala ├── project ├── Build.scala ├── build.properties └── plugins.sbt ├── scripts └── check-env-variables.sh ├── version.sh └── version.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache 6 | .history 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | .DS_Store 16 | 17 | # Scala-IDE specific 18 | .scala_dependencies 19 | .worksheet 20 | 21 | /.idea 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 pico-works 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pico-event 2 | [![CircleCI](https://circleci.com/gh/pico-works/pico-event/tree/develop.svg?style=svg)](https://circleci.com/gh/pico-works/pico-event/tree/develop) 3 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/492233dcb0824733a7cb7b60468ae418)](https://www.codacy.com/app/newhoggy/pico-works-pico-event?utm_source=github.com&utm_medium=referral&utm_content=pico-works/pico-event&utm_campaign=Badge_Grade) 4 | [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/492233dcb0824733a7cb7b60468ae418)](https://www.codacy.com/app/newhoggy/pico-works-pico-event?utm_source=github.com&utm_medium=referral&utm_content=pico-works/pico-event&utm_campaign=Badge_Coverage) 5 | [![Gitter chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/pico-works/general) 6 | 7 | Support library for atomic operations. 8 | 9 | ## Getting started 10 | 11 | Add this to your SBT project: 12 | 13 | ``` 14 | resolvers += "dl-john-ky-releases" at "http://dl.john-ky.io/maven/releases" 15 | 16 | libraryDependencies += "org.pico" %% "pico-event" % "2.0.2" 17 | ``` 18 | 19 | Then read the [tutorial](pico-event/src/main/tut/tutorial.md). 20 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | organization in ThisBuild := "org.pico" 2 | 3 | scalaVersion in ThisBuild := "2.11.8" 4 | 5 | crossScalaVersions in ThisBuild := Seq("2.10.6", "2.11.8", "2.12.0") 6 | 7 | version in ThisBuild := Process("./version.sh").lines.head.trim 8 | 9 | resolvers in ThisBuild ++= Seq( 10 | "bintray/non" at "http://dl.bintray.com/non/maven", 11 | "dl-john-ky-releases" at "http://dl.john-ky.io/maven/releases", 12 | "dl-john-ky-snapshots" at "http://dl.john-ky.io/maven/snapshots") 13 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | cache_directories: 3 | - ~/.sbt 4 | - ~/.coursier 5 | 6 | pre: 7 | - scripts/check-env-variables.sh 8 | - git fetch --unshallow || true 9 | 10 | override: 11 | - sbt -batch +test:compile +package 12 | 13 | post: 14 | - find ~/.sbt -name "*.lock" | xargs rm 15 | - find ~/.ivy2 -name "ivydata-*.properties" | sed 's/ /\\ /g' | sed 's/)/\\)/g' | xargs rm 16 | 17 | test: 18 | override: 19 | - sbt +test 20 | 21 | post: 22 | - sbt coverage clean test coverageReport coverageAggregate 23 | - mkdir -p target/scala-2.11/coverage-report/ 24 | - sbt codacyCoverage 25 | - bash <(curl -s https://codecov.io/bash) 26 | 27 | deployment: 28 | release: 29 | owner: pico-works 30 | tag: /v.*/ 31 | commands: 32 | - sbt +publish 33 | 34 | integration: 35 | owner: pico-works 36 | branch: [develop, master] 37 | commands: 38 | - sbt +publish 39 | 40 | development: 41 | branch: /PR-.*/ 42 | commands: 43 | - sbt +publish 44 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/Bus.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | object Bus { 4 | /** Create a Bus of the given type. 5 | */ 6 | def apply[A]: Bus[A] = SinkSource[A, A](identity) 7 | } 8 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/Cell.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import java.util.concurrent.atomic.AtomicReference 4 | 5 | import org.pico.atomic.syntax.std.atomicReference._ 6 | 7 | @specialized(Boolean, Long, Double) 8 | trait Cell[A] extends View[A] { 9 | def value: A 10 | 11 | def value_=(that: A): Unit 12 | 13 | def update(f: A => A): (A, A) 14 | 15 | def updateIf(cond: A => Boolean, f: A => A): Option[(A, A)] 16 | 17 | def getAndSet(a: A): A 18 | 19 | def compareAndSet(expect: A, update: A): Boolean 20 | } 21 | 22 | object Cell { 23 | def apply[A](initial: A): Cell[A] = { 24 | new Cell[A] { 25 | val valueRef = new AtomicReference[A](initial) 26 | 27 | override def value: A = { 28 | invalidations.validate() 29 | valueRef.get() 30 | } 31 | 32 | override def value_=(that: A): Unit = { 33 | valueRef.set(that) 34 | invalidations.invalidate() 35 | } 36 | 37 | override def update(f: A => A): (A, A) = { 38 | val result = valueRef.update(f) 39 | invalidations.invalidate() 40 | result 41 | } 42 | 43 | override def updateIf(cond: A => Boolean, f: A => A): Option[(A, A)] = { 44 | val result = valueRef.updateIf(cond, f) 45 | result.foreach(_ => invalidations.invalidate()) 46 | result 47 | } 48 | 49 | override def getAndSet(a: A): A = { 50 | val result = valueRef.getAndSet(a) 51 | invalidations.invalidate() 52 | result 53 | } 54 | 55 | override def compareAndSet(expect: A, update: A): Boolean = { 56 | val result = valueRef.compareAndSet(expect, update) 57 | if (result) { 58 | invalidations.invalidate() 59 | } 60 | result 61 | } 62 | 63 | override lazy val invalidations: Invalidations = Invalidations() 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/ClosedSink.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import org.pico.disposal.ClosedDisposer 4 | 5 | trait ClosedSink extends Sink[Any] with ClosedDisposer { 6 | override def publish(event: Any): Unit = () 7 | 8 | override def comap[B](f: B => Any): Sink[B] = this 9 | } 10 | 11 | /** An already closed sink that will ignore any events published to it. 12 | */ 13 | object ClosedSink extends ClosedSink 14 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/ClosedSinkSource.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | trait ClosedSinkSource extends SinkSource[Any, Nothing] with ClosedSink with ClosedSource 4 | 5 | /** An already closed sink source that ignores any events published to it, never emits events and 6 | * never hold references to any subscribers it is given. 7 | */ 8 | object ClosedSinkSource extends ClosedSinkSource 9 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/ClosedSource.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import java.io.Closeable 4 | 5 | import org.pico.disposal.{Closed, ClosedDisposer} 6 | import org.pico.disposal.std.autoCloseable._ 7 | 8 | trait ClosedSource extends Source[Nothing] with ClosedDisposer { 9 | override def subscribe(subscriber: Nothing => Unit): Closeable = Closed 10 | 11 | override def map[B](f: Nothing => B): Source[B] = ClosedSource 12 | 13 | override def effect(f: Nothing => Unit): Source[Nothing] = ClosedSource 14 | 15 | override def mapConcat[F[_]: HasForeach, B](f: Nothing => F[B]): Source[B] = ClosedSource 16 | 17 | override def merge[B](that: Source[B]): Source[B] = that 18 | 19 | override def foldRight[B](initial: B)(f: (Nothing, => B) => B): View[B] = View(initial) 20 | 21 | override def into[B >: Nothing](sink: Sink[B]): Closeable = Closed 22 | 23 | override def filter(f: Nothing => Boolean): Source[Nothing] = ClosedSource 24 | 25 | override def or[B](that: Source[B]): Source[Either[Nothing, B]] = { 26 | val temp = Bus[Either[Nothing, B]] 27 | temp += that.subscribe(e => temp.publish(Right(e))) 28 | temp 29 | } 30 | } 31 | 32 | /** An already closed source that will never emit events or hold references to any subscribers it 33 | * is given. 34 | */ 35 | object ClosedSource extends ClosedSource 36 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/ClosedSubscribers.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import java.io.Closeable 4 | 5 | import org.pico.disposal.{Closed, Disposer} 6 | 7 | object ClosedSubscribers extends Subscribers[Any, Nothing] with Disposer { 8 | override def subscribe(subscriber: (Nothing) => Unit): Closeable = Closed 9 | 10 | override def publish(event: Any): Unit = () 11 | 12 | override def houseKeep(): Unit = () 13 | } 14 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/CompositeSinkSource.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import java.io.Closeable 4 | 5 | import org.pico.disposal.SimpleDisposer 6 | import org.pico.disposal.std.autoCloseable._ 7 | 8 | trait CompositeSinkSource[A, B] extends SinkSource[A, B] with SimpleDisposer { 9 | /** Publish an event to a sink. 10 | */ 11 | override def publish(event: A): Unit = asSink.publish(event) 12 | 13 | /** Subscribe a subscriber to a source. The subscriber will be invoked with any events that the 14 | * source may emit. 15 | */ 16 | override def subscribe(subscriber: B => Unit): Closeable = asSource.subscribe(subscriber) 17 | } 18 | 19 | object CompositeSinkSource { 20 | def from[A, B](sink: Sink[A], source: Source[B]): SinkSource[A, B] = { 21 | new CompositeSinkSource[A, B] { self => 22 | override val asSink: Sink[A] = self.disposes(sink) 23 | 24 | override val asSource: Source[B] = self.disposes(source) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/ComputedView.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import java.util.concurrent.atomic.AtomicReference 4 | 5 | trait ComputedView[A] extends View[A] { 6 | private val ref = new AtomicReference[A](compute()) 7 | 8 | def compute(): A 9 | 10 | final def invalidate(): Unit = invalidations.invalidate() 11 | 12 | final override def value: A = { 13 | if (invalidations.valid) { 14 | ref.get() 15 | } else { 16 | invalidations.validate() 17 | val newValue = compute() 18 | ref.set(newValue) 19 | newValue 20 | } 21 | } 22 | 23 | final override val invalidations: Invalidations = Invalidations() 24 | } 25 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/HasForeach.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import scala.language.higherKinds 4 | 5 | trait HasForeach[F[_]] { 6 | def foreach[A](self: F[A])(f: A => Unit): Unit 7 | } 8 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/IntCell.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import java.util.concurrent.atomic.AtomicInteger 4 | 5 | import org.pico.atomic.syntax.std.atomicInteger._ 6 | 7 | final class IntCell(initial: Int) extends Cell[Int] { 8 | val valueRef = new AtomicInteger(initial) 9 | 10 | override def value: Int = { 11 | invalidations.validate() 12 | valueRef.get() 13 | } 14 | 15 | override def value_=(that: Int): Unit = { 16 | valueRef.set(that) 17 | invalidations.invalidate() 18 | } 19 | 20 | override def update(f: Int => Int): (Int, Int) = { 21 | val result = valueRef.update(f) 22 | invalidations.invalidate() 23 | result 24 | } 25 | 26 | def updateIf(cond: Int => Boolean, f: Int => Int): Option[(Int, Int)] = { 27 | val result = valueRef.updateIf(cond, f) 28 | result.foreach(_ => invalidations.invalidate()) 29 | result 30 | } 31 | 32 | override def getAndSet(a: Int): Int = { 33 | val result = valueRef.getAndSet(a) 34 | invalidations.invalidate() 35 | result 36 | } 37 | 38 | override def compareAndSet(expect: Int, update: Int): Boolean = { 39 | val result = valueRef.compareAndSet(expect, update) 40 | if (result) { 41 | invalidations.invalidate() 42 | } 43 | result 44 | } 45 | 46 | override lazy val invalidations: Invalidations = Invalidations() 47 | 48 | def incrementAndGet(): Int = { 49 | val result = valueRef.incrementAndGet() 50 | invalidations.invalidate() 51 | result 52 | } 53 | 54 | def decrementAndGet(): Int = { 55 | val result = valueRef.decrementAndGet() 56 | invalidations.invalidate() 57 | result 58 | } 59 | 60 | def getAndIncrementAnd(): Int = { 61 | val result = valueRef.getAndIncrement() 62 | invalidations.invalidate() 63 | result 64 | } 65 | 66 | def getAndDecrementAnd(): Int = { 67 | val result = valueRef.getAndDecrement() 68 | invalidations.invalidate() 69 | result 70 | } 71 | 72 | def addAndGet(that: Int): Int = { 73 | val result = valueRef.addAndGet(that) 74 | invalidations.invalidate() 75 | result 76 | } 77 | 78 | def getAndAdd(that: Int): Int = { 79 | val result = valueRef.getAndAdd(that) 80 | invalidations.invalidate() 81 | result 82 | } 83 | } 84 | 85 | object IntCell { 86 | def apply(initial: Int): IntCell = new IntCell(initial) 87 | } 88 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/Invalidations.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import java.util.concurrent.atomic.AtomicBoolean 4 | 5 | trait Invalidations extends Source[Unit] { 6 | def valid: Boolean 7 | 8 | def invalid: Boolean = !valid 9 | 10 | def validate(): Unit 11 | 12 | def invalidate(): Unit 13 | } 14 | 15 | object Invalidations { 16 | def apply(): Invalidations = { 17 | new Invalidations with SimpleSinkSource[Unit, Unit] { 18 | val isValid = new AtomicBoolean(false) 19 | 20 | def valid = isValid.get() 21 | 22 | def validate(): Unit = isValid.set(true) 23 | 24 | override def invalidate(): Unit = { 25 | if (isValid.getAndSet(false)) { 26 | this.publish(()) 27 | } 28 | } 29 | 30 | override def transform: Unit => Unit = identity 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/LongCell.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import java.util.concurrent.atomic.AtomicLong 4 | 5 | import org.pico.atomic.syntax.std.atomicLong._ 6 | 7 | final class LongCell(initial: Long) extends Cell[Long] { 8 | val valueRef = new AtomicLong(initial) 9 | 10 | override def value: Long = { 11 | invalidations.validate() 12 | valueRef.get() 13 | } 14 | 15 | override def value_=(that: Long): Unit = { 16 | valueRef.set(that) 17 | invalidations.invalidate() 18 | } 19 | 20 | override def update(f: Long => Long): (Long, Long) = { 21 | val result = valueRef.update(f) 22 | invalidations.invalidate() 23 | result 24 | } 25 | 26 | def updateIf(cond: Long => Boolean, f: Long => Long): Option[(Long, Long)] = { 27 | val result = valueRef.updateIf(cond, f) 28 | result.foreach(_ => invalidations.invalidate()) 29 | result 30 | } 31 | 32 | override def getAndSet(a: Long): Long = { 33 | val result = valueRef.getAndSet(a) 34 | invalidations.invalidate() 35 | result 36 | } 37 | 38 | override def compareAndSet(expect: Long, update: Long): Boolean = { 39 | val result = valueRef.compareAndSet(expect, update) 40 | if (result) { 41 | invalidations.invalidate() 42 | } 43 | result 44 | } 45 | 46 | override lazy val invalidations: Invalidations = Invalidations() 47 | 48 | def incrementAndGet(): Long = { 49 | val result = valueRef.incrementAndGet() 50 | invalidations.invalidate() 51 | result 52 | } 53 | 54 | def decrementAndGet(): Long = { 55 | val result = valueRef.decrementAndGet() 56 | invalidations.invalidate() 57 | result 58 | } 59 | 60 | def getAndIncrementAnd(): Long = { 61 | val result = valueRef.getAndIncrement() 62 | invalidations.invalidate() 63 | result 64 | } 65 | 66 | def getAndDecrementAnd(): Long = { 67 | val result = valueRef.getAndDecrement() 68 | invalidations.invalidate() 69 | result 70 | } 71 | 72 | def addAndGet(that: Long): Long = { 73 | val result = valueRef.addAndGet(that) 74 | invalidations.invalidate() 75 | result 76 | } 77 | 78 | def getAndAdd(that: Long): Long = { 79 | val result = valueRef.getAndAdd(that) 80 | invalidations.invalidate() 81 | result 82 | } 83 | } 84 | 85 | object LongCell { 86 | def apply(initial: Long): LongCell = new LongCell(initial) 87 | } 88 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/SimpleBus.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | trait SimpleBus[A] extends SimpleSinkSource[A, A] { 4 | override def transform: A => A = identity 5 | } 6 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/SimpleSinkSource.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import java.io.Closeable 4 | import java.util.concurrent.atomic.AtomicReference 5 | 6 | import org.pico.disposal.SimpleDisposer 7 | import org.pico.disposal.std.autoCloseable._ 8 | 9 | trait SimpleSinkSource[A, B] extends SinkSource[A, B] with SimpleDisposer { 10 | val impl = this.swapDisposes(ClosedSubscribers, new AtomicReference(Subscribers[A, B](transform))) 11 | 12 | impl.get().disposes(this) 13 | 14 | def transform: A => B 15 | 16 | override def publish(event: A): Unit = impl.get().publish(event) 17 | 18 | override def subscribe(subscriber: B => Unit): Closeable = impl.get().subscribe(subscriber) 19 | } 20 | 21 | object SimpleSinkSource { 22 | def apply[A, B](f: A => B): SinkSource[A, B] = { 23 | new SimpleSinkSource[A, B] { 24 | override def transform: A => B = f 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/Sink.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} 4 | 5 | import org.pico.disposal.std.autoCloseable._ 6 | import org.pico.disposal.{Disposer, SimpleDisposer} 7 | 8 | trait Sink[-A] extends Disposer { self => 9 | /** Get the Sink representation of this. 10 | */ 11 | def asSink: Sink[A] = this 12 | 13 | /** Publish an event to a sink. 14 | */ 15 | def publish(event: A): Unit 16 | 17 | /** Create a new Sink that applies a function to the event before propagating it to the 18 | * original sink. 19 | */ 20 | def comap[B](f: B => A): Sink[B] = new Sink[B] with SimpleDisposer { temp => 21 | val selfRef = temp.swapDisposes(ClosedSinkSource, new AtomicReference(self)) 22 | override def publish(event: B): Unit = selfRef.get().publish(f(event)) 23 | } 24 | } 25 | 26 | object Sink { 27 | /** Create a sink that calls the side-effecting function for every event emitted. 28 | * 29 | * @param f The side-effecting function 30 | * @tparam A The type of the event 31 | * @return A sink that invokes the side-effecting function for every event emitted. 32 | */ 33 | def apply[A](f: A => Unit): Sink[A] = { 34 | new Sink[A] with SimpleDisposer { 35 | val active = new AtomicBoolean(true) 36 | 37 | this.onClose(active.set(false)) 38 | 39 | override def publish(event: A): Unit = if (active.get()) f(event) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/SinkSource.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | /** A SinkSource is both a Sink and a Source. 4 | * Any events published to the SinkSource will have a transformation function applied to it 5 | * before emitting the transformed event to subscribers. 6 | */ 7 | trait SinkSource[-A, +B] extends Sink[A] with Source[B] 8 | 9 | object SinkSource { 10 | def apply[A, B](f: A => B): SinkSource[A, B] = SimpleSinkSource(f) 11 | 12 | def from[A, B](sink: Sink[A], source: Source[B]): SinkSource[A, B] = CompositeSinkSource.from(sink, source) 13 | } 14 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/Source.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import java.io.Closeable 4 | 5 | import org.pico.disposal.Disposer 6 | import org.pico.disposal.std.autoCloseable._ 7 | import org.pico.disposal.syntax.disposable._ 8 | import org.pico.event.syntax.hasForeach._ 9 | 10 | import scala.language.higherKinds 11 | import org.pico.atomic.syntax.std.atomicReference._ 12 | 13 | trait Source[+A] extends Disposer { self => 14 | /** Get the Source representation of this. 15 | */ 16 | def asSource: Source[A] = this 17 | 18 | /** Subscribe a subscriber to a source. The subscriber will be invoked with any events that the 19 | * source may emit. 20 | */ 21 | def subscribe(subscriber: A => Unit): Closeable 22 | 23 | /** Create a new Source that will emit transformed events that have been emitted by the original 24 | * Source. The transformation is described by the function argument. 25 | */ 26 | def map[B](f: A => B): Source[B] = { 27 | new SimpleSinkSource[A, B] { temp => 28 | override def transform: A => B = f 29 | 30 | disposes(self.subscribe(temp.publish)) 31 | } 32 | } 33 | 34 | /** Side effect to execute when an event occurs. 35 | * 36 | * @param f The side effecting function 37 | * @return a source that emits the same events after the side effect has been performed 38 | */ 39 | def effect(f: A => Unit): Source[A] = self.map { a => f(a); a } 40 | 41 | /** From a function that maps each event into an iterable event, create a new Source that will 42 | * emit each element of the iterable event. 43 | */ 44 | def mapConcat[F[_]: HasForeach, B](f: A => F[B]): Source[B] = { 45 | new SimpleBus[B] { temp => 46 | temp += self.subscribe(f(_).foreach(temp.publish)) 47 | } 48 | } 49 | 50 | /** Compose two sources of compatible type together into a new source that emits the same events 51 | * as either of the two originals. 52 | */ 53 | def merge[B >: A](that: Source[B]): Source[B] = { 54 | new SimpleBus[B] { temp => 55 | temp += self.subscribe(temp.publish) :+: that.subscribe(temp.publish) 56 | } 57 | } 58 | 59 | /** Fold the event source into a value given the value's initial state. 60 | * 61 | * @param f The folding function 62 | * @param initial The initial state 63 | * @tparam B Type of the new value 64 | * @return The value. 65 | */ 66 | def foldRight[B](initial: B)(f: (A, => B) => B): View[B] = { 67 | val cell = Cell[B](initial) 68 | 69 | cell.disposes(this.subscribe(v => cell.update(a => f(v, a)))) 70 | 71 | cell 72 | } 73 | 74 | /** Direct all events into the sink. 75 | */ 76 | def into[B >: A](sink: Sink[B]): Closeable = self.subscribe(sink.publish) 77 | 78 | /** Create a new Source that emits only events that satisfies the predicate f 79 | * 80 | * @param f The predicate 81 | * @return New filtering source 82 | */ 83 | def filter(f: A => Boolean): Source[A] = new SimpleBus[A] { temp => 84 | temp += self.subscribe(a => if (f(a)) temp.publish(a)) 85 | } 86 | 87 | /** Merge to sources such that events emitted from the left source will be emitted in the Left 88 | * case and events emitted from the right source will be emitted in the Right case. 89 | * 90 | * @param that The right source 91 | * @tparam B The type of the right source events 92 | * @return A new source that emits events from left and right sources. 93 | */ 94 | def or[B](that: Source[B]): Source[Either[A, B]] = { 95 | val temp = Bus[Either[A, B]] 96 | temp += self.subscribe(e => temp.publish(Left(e))) 97 | temp += that.subscribe(e => temp.publish(Right(e))) 98 | temp 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/Subscribers.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import java.io.Closeable 4 | import java.lang.ref.WeakReference 5 | import java.util.concurrent.atomic.{AtomicInteger, AtomicReference} 6 | 7 | import org.pico.atomic.syntax.std.atomicReference._ 8 | import org.pico.disposal.{OnClose, SimpleDisposer} 9 | 10 | /** A simple SinkSource which implements subscriber tracking. 11 | * 12 | * This implementation does not release references have a well-defined closed state. 13 | * The SimpleSinkSource wrapper type will properly define the closed state. 14 | * 15 | * @tparam A The sink event type 16 | * @tparam B The source event type 17 | */ 18 | trait Subscribers[-A, +B] extends SimpleDisposer { 19 | def subscribe(subscriber: (B) => Unit): Closeable 20 | 21 | def publish(event: A): Unit 22 | 23 | def houseKeep(): Unit 24 | } 25 | 26 | object Subscribers { 27 | def apply[A, B](f: A => B): Subscribers[A, B] = { 28 | new Subscribers[A, B] { 29 | val subscribers = new AtomicReference(List.empty[WeakReference[Wrapper[B => Unit]]]) 30 | val garbage = new AtomicInteger(0) 31 | 32 | def transform: A => B = f 33 | 34 | override def subscribe(subscriber: (B) => Unit): Closeable = { 35 | val wrapper = Wrapper(subscriber) 36 | val subscriberRef = new WeakReference(wrapper) 37 | 38 | subscribers.update(subscriberRef :: _) 39 | 40 | houseKeep() 41 | 42 | OnClose { 43 | identity(wrapper) 44 | subscriberRef.clear() 45 | houseKeep() 46 | } 47 | } 48 | 49 | override def publish(event: A): Unit = { 50 | val v = transform(event) 51 | 52 | subscribers.get().foreach { subscriberRef => 53 | var wrapper = subscriberRef.get() 54 | 55 | if (wrapper != null) { 56 | val subscriber = wrapper.target 57 | // Drop reference to wrapper so that the garbage collector can collect it if there are no 58 | // other references to it. This helps facilitate earlier collection, especially if a lot 59 | // of time is spent in the subscriber. This is why wrapper is a var. 60 | wrapper = null 61 | subscriber(v) 62 | } else { 63 | garbage.incrementAndGet() 64 | } 65 | } 66 | 67 | houseKeep() 68 | } 69 | 70 | override def houseKeep(): Unit = { 71 | if (garbage.get() > subscribers.get().size) { 72 | garbage.set(0) 73 | subscribers.update { subscriptions => 74 | subscriptions.filter { subscription => 75 | subscription.get() != null 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/TimedBus.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import java.io.Closeable 4 | import java.util.concurrent.atomic.AtomicReference 5 | 6 | import org.pico.disposal.SimpleDisposer 7 | import org.pico.disposal.std.autoCloseable._ 8 | 9 | import scala.concurrent.duration.{Deadline, Duration} 10 | 11 | trait TimedBus[A] extends Bus[A] with SimpleDisposer { 12 | val impl = this.swapDisposes(ClosedSubscribers, new AtomicReference(Subscribers[A, A](identity))) 13 | 14 | impl.get().disposes(this) 15 | 16 | override def publish(event: A): Unit = impl.get().publish(event) 17 | 18 | override def subscribe(subscriber: A => Unit): Closeable = impl.get().subscribe(subscriber) 19 | } 20 | 21 | object TimedBus { 22 | def apply[A](times: Sink[Duration]): Bus[A] = { 23 | new TimedBus[A] { 24 | override def publish(event: A): Unit = { 25 | val start = Deadline.now 26 | 27 | try { 28 | super.publish(event) 29 | } finally { 30 | times.publish(Deadline.now - start) 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/View.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import cats.Applicative 4 | import org.pico.disposal.SimpleDisposer 5 | import org.pico.disposal.std.autoCloseable._ 6 | 7 | @specialized(Boolean, Long, Double) 8 | trait View[+A] extends SimpleDisposer { self => 9 | def asView: View[A] = this 10 | 11 | def value: A 12 | 13 | def invalidations: Source[Unit] 14 | 15 | lazy val source = this.disposes(invalidations.map(_ => value)) 16 | } 17 | 18 | object View { 19 | implicit val applicativeView_JZ4YLf6 = new Applicative[View] { 20 | override def map[A, B](fa: View[A])(f: A => B): View[B] = { 21 | new ComputedView[B] { 22 | this.disposes(fa.invalidations.subscribe(_ => invalidate())) 23 | 24 | override def compute(): B = f(fa.value) 25 | } 26 | } 27 | 28 | override def pure[A](x: A): View[A] = View(x) 29 | 30 | override def ap[A, B](ff: View[A => B])(fa: View[A]): View[B] = { 31 | new ComputedView[B] { 32 | this.disposes(fa.invalidations.subscribe(_ => invalidate())) 33 | this.disposes(ff.invalidations.subscribe(_ => invalidate())) 34 | 35 | override def compute(): B = ff.value(fa.value) 36 | } 37 | } 38 | 39 | // override def flatMap[A, B](fa: View[A])(f: A => View[B]): View[B] = { 40 | // new ComputedView[B] { 41 | // override def compute(): B = f(fa.value).value 42 | // 43 | // val subscriptionRef = this.swapDisposes(Closed, new AtomicReference(f(fa.value).invalidations.subscribe(_ -> invalidations.invalidate()))) 44 | // val lock = new Object 45 | // 46 | // this += fa.invalidations.subscribe { _ => 47 | // lock.synchronized { 48 | // subscriptionRef.swap(Closed).dispose() 49 | // val viewB = f(fa.value) 50 | // subscriptionRef.swap(viewB.invalidations.subscribe(_ -> invalidations.invalidate())) 51 | // } 52 | // } 53 | // } 54 | // } 55 | 56 | // override def tailRecM[A, B](a: A)(f: A => View[Either[A, B]]): View[B] = defaultTailRecM(a)(f) 57 | } 58 | 59 | def apply[A](initial: A): View[A] = { 60 | new View[A] { 61 | override def value: A = initial 62 | 63 | override def invalidations: Source[Unit] = ClosedSource 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/WaitCompleteSink.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import org.pico.disposal.SimpleDisposer 4 | 5 | import scala.concurrent.ExecutionContext.Implicits.global 6 | import scala.concurrent.Future 7 | 8 | /** A sink that will wait when closed for all futures published to it to complete. 9 | */ 10 | trait WaitCompleteSink[-A] extends Sink[Future[A]] with AutoCloseable 11 | 12 | object WaitCompleteSink { 13 | def apply[A]: WaitCompleteSink[A] = { 14 | new WaitCompleteSink[A] with SimpleDisposer { 15 | private val lock = new Object 16 | private var done = false 17 | private var inFlight = 0L 18 | 19 | this.onClose { 20 | lock synchronized { 21 | done = true 22 | 23 | while (inFlight > 0L) { 24 | lock.wait() 25 | } 26 | } 27 | } 28 | 29 | override def publish(event: Future[A]): Unit = { 30 | lock synchronized { 31 | if (!done) { 32 | inFlight += 1 33 | } 34 | } 35 | 36 | event.onComplete { completion => 37 | lock synchronized { 38 | inFlight -= 1 39 | lock.notifyAll() 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/Wrapper.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | case class Wrapper[A](target: A) 4 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/concurrent/ExecutionContextBus.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event.concurrent 2 | 3 | import org.pico.event.{Bus, Sink, SinkSource} 4 | 5 | import scala.concurrent.ExecutionContext 6 | 7 | object ExecutionContextBus { 8 | def apply[A](implicit ec: ExecutionContext): Bus[A] = { 9 | val target = Bus[A] 10 | 11 | val sink = Sink[A] { a => 12 | ec.execute(new Runnable { 13 | override def run(): Unit = target.publish(a) 14 | }) 15 | } 16 | 17 | SinkSource.from(sink, target) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/io/ByteCountOutputStream.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event.io 2 | 3 | import java.io.OutputStream 4 | 5 | import org.pico.event.Sink 6 | 7 | object ByteCountOutputStream { 8 | /** Decorate an output stream with a version that counts how many bytes have been written. 9 | */ 10 | def apply( 11 | os: OutputStream, 12 | bytesWritten: Sink[Long]): OutputStream = { 13 | new OutputStream { 14 | override def write(byte: Int): Unit = { 15 | os.write(byte) 16 | bytesWritten.publish(1L) 17 | } 18 | 19 | override def write(buffer: Array[Byte]): Unit = { 20 | os.write(buffer) 21 | bytesWritten.publish(buffer.length.toLong) 22 | } 23 | 24 | override def write(buffer: Array[Byte], offset: Int, length: Int): Unit = { 25 | os.write(buffer, offset, length) 26 | bytesWritten.publish(length.toLong) 27 | } 28 | 29 | override def flush(): Unit = os.flush() 30 | 31 | override def close(): Unit = os.close() 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/io/NewlineCountWriter.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event.io 2 | 3 | 4 | import java.io.Writer 5 | 6 | import org.pico.event.{ClosedSink, Sink} 7 | 8 | object NewlineCountWriter { 9 | /** Decorate a writer with a version that counts how many new lines have been written. 10 | */ 11 | def apply( 12 | os: Writer, 13 | newLines: Sink[Long] = ClosedSink): Writer = { 14 | new Writer { 15 | override def flush(): Unit = os.flush() 16 | 17 | override def write(cbuf: Array[Char], off: Int, len: Int): Unit = { 18 | val end = off + len 19 | 20 | var i = off 21 | 22 | while (i < end) { 23 | if (cbuf(i) == '\n') { 24 | newLines.publish(1L) 25 | } 26 | 27 | i += 1 28 | } 29 | 30 | os.write(cbuf, off, len) 31 | } 32 | 33 | override def close(): Unit = os.close() 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/net/UdpEmitFailed.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event.net 2 | 3 | import java.net.InetSocketAddress 4 | import java.nio.ByteBuffer 5 | 6 | case class UdpEmitFailed( 7 | address: InetSocketAddress, 8 | buffer: ByteBuffer, 9 | sentBytes: Int) 10 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/net/UdpEmitter.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event.net 2 | 3 | import java.net.InetSocketAddress 4 | import java.nio.ByteBuffer 5 | import java.nio.channels.DatagramChannel 6 | 7 | import org.pico.disposal.std.autoCloseable._ 8 | import org.pico.event.{Bus, Sink, SinkSource} 9 | 10 | object UdpEmitter { 11 | /** Create [[SinkSource]] that emits UDP packets when publishing to its sink and reports failures 12 | * from its source. 13 | */ 14 | def apply(addressLookup: () => InetSocketAddress): SinkSource[ByteBuffer, UdpEmitFailed] = { 15 | val clientChannel = DatagramChannel.open 16 | 17 | val errors = Bus[UdpEmitFailed] 18 | 19 | val sink = Sink[ByteBuffer] { buffer => 20 | val address = addressLookup() 21 | val sentBytes = clientChannel.send(buffer, address) 22 | 23 | if (buffer.limit() != sentBytes) { 24 | errors.publish(UdpEmitFailed(address, buffer, sentBytes)) 25 | } 26 | } 27 | 28 | sink.disposes(clientChannel) 29 | 30 | SinkSource.from[ByteBuffer, UdpEmitFailed](sink, errors) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/package.scala: -------------------------------------------------------------------------------- 1 | package org.pico 2 | 3 | package object event { 4 | /** A Bus is a SinkSource where the Sink event type is the same as the Source event type. 5 | */ 6 | type Bus[A] = SinkSource[A, A] 7 | } 8 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/std/all/package.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event.std 2 | 3 | import org.pico.event.HasForeach 4 | 5 | package object all { 6 | implicit val hasForEach_Option_Nsz7ia9 = new HasForeach[Option] { 7 | override def foreach[A](self: Option[A])(f: (A) => Unit): Unit = self.foreach(f) 8 | } 9 | 10 | implicit val hasForEach_List_Nsz7ia9 = new HasForeach[List] { 11 | override def foreach[A](self: List[A])(f: (A) => Unit): Unit = self.foreach(f) 12 | } 13 | 14 | implicit val hasForEach_Iterable_Nsz7ia9 = new HasForeach[Iterable] { 15 | override def foreach[A](self: Iterable[A])(f: (A) => Unit): Unit = self.foreach(f) 16 | } 17 | 18 | implicit val hasForEach_Stream_Nsz7ia9 = new HasForeach[Stream] { 19 | override def foreach[A](self: Stream[A])(f: (A) => Unit): Unit = self.foreach(f) 20 | } 21 | 22 | implicit val hasForEach_Seq_Nsz7ia9 = new HasForeach[Seq] { 23 | override def foreach[A](self: Seq[A])(f: (A) => Unit): Unit = self.foreach(f) 24 | } 25 | 26 | implicit val hasForEach_Vector_Nsz7ia9 = new HasForeach[Vector] { 27 | override def foreach[A](self: Vector[A])(f: (A) => Unit): Unit = self.foreach(f) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/syntax/disposer/package.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event.syntax 2 | 3 | import org.pico.disposal.Disposer 4 | import org.pico.event.Cell 5 | 6 | package object disposer { 7 | implicit class DisposerOps_dEhxmsY(val self: Disposer) extends AnyVal { 8 | /** Register an var reference for reset on close. When the disposer is closed, the 9 | * replacement value is swapped in. 10 | * 11 | * @param replacement The replacement value to use when swapping 12 | * @param variable The reference to swap 13 | * @tparam V The type of the value 14 | * @return The disposable object 15 | */ 16 | @inline 17 | final def resets[V](replacement: V, variable: Cell[V]): Cell[V] = { 18 | self.onClose(variable.value = replacement) 19 | variable 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/syntax/future/package.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event.syntax 2 | 3 | import org.pico.event.Sink 4 | 5 | import scala.concurrent.ExecutionContext.Implicits.global 6 | import scala.concurrent.Future 7 | 8 | package object future { 9 | implicit class FutureOps_2Qos8tq[A](val self: Future[A]) extends AnyVal { 10 | def successInto(sink: Sink[A]): Future[A] = { 11 | self.foreach { value => 12 | sink.publish(value) 13 | } 14 | 15 | self 16 | } 17 | 18 | def failureInto(sink: Sink[Throwable]): Future[A] = { 19 | self.onFailure { case value => 20 | sink.publish(value) 21 | } 22 | 23 | self 24 | } 25 | 26 | def completeInto(successSink: Sink[A], failureSink: Sink[Throwable]): Future[A] = { 27 | self.successInto(successSink).failureInto(failureSink) 28 | } 29 | 30 | def completeInto(sink: Sink[Either[Throwable, A]]): Future[A] = { 31 | self.successInto(sink.comap(Right(_))).failureInto(sink.comap(Left(_))) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/syntax/hasForeach/package.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event.syntax 2 | 3 | import org.pico.event.HasForeach 4 | 5 | import scala.language.higherKinds 6 | 7 | package object hasForeach { 8 | implicit class HasForeachOps_fX9WgUY6[F[_], A](val self: F[A]) extends AnyVal { 9 | @inline def foreach(f: A => Unit)(implicit ev: HasForeach[F]): Unit = ev.foreach(self)(f) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/syntax/outputStream/package.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event.syntax 2 | 3 | import java.io.OutputStream 4 | 5 | import org.pico.event.Sink 6 | import org.pico.event.io.ByteCountOutputStream 7 | 8 | package object outputStream { 9 | implicit class OutputStreamOps_3hXLiwd(val self: OutputStream) extends AnyVal { 10 | /** Decorate an output stream with the behaviour of counting bytes as they are written and publishing the counts 11 | * to the provided sink. 12 | */ 13 | @inline final def countBytesIn(sink: Sink[Long]): OutputStream = ByteCountOutputStream(self, sink) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/syntax/sink/package.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event.syntax 2 | 3 | import java.util.concurrent.atomic.AtomicReference 4 | 5 | import org.pico.atomic.syntax.std.atomicReference._ 6 | import org.pico.event.{ClosedSink, Sink} 7 | 8 | import scala.concurrent.ExecutionContext 9 | 10 | package object sink { 11 | implicit class SinkOps_cHdm8Nd[A](val self: Sink[A]) extends AnyVal { 12 | /** Create a new sink that only publishes events that satisfy the predicate f. 13 | * 14 | * @param f The predicate 15 | * @return The new filtering sink. 16 | */ 17 | def cofilter(f: A => Boolean): Sink[A] = Sink[A](a => if (f(a)) self.publish(a)) 18 | 19 | /** Create a sink that publishes to one of two sinks depending on the side of the Either. 20 | */ 21 | def fork[B](that: Sink[B]): Sink[Either[A, B]] = { 22 | val refSelf = new AtomicReference[Sink[A]](self) 23 | val refThat = new AtomicReference[Sink[B]](that) 24 | 25 | val sink = Sink[Either[A, B]] { 26 | case Left(a) => Option(refSelf.get()).foreach(_.publish(a)) 27 | case Right(b) => Option(refThat.get()).foreach(_.publish(b)) 28 | } 29 | 30 | sink.resets(ClosedSink, refSelf) 31 | sink.resets(ClosedSink, refThat) 32 | 33 | sink 34 | } 35 | 36 | /** Create a sink that handles its message by publishing to the original sink via the specified 37 | * execution context. 38 | */ 39 | def coVia(executor: ExecutionContext): Sink[A] = { 40 | val selfRef = new AtomicReference(self) 41 | 42 | val sink = Sink[A] { a => 43 | executor.execute(new Runnable { 44 | override def run(): Unit = selfRef.value.publish(a) 45 | }) 46 | } 47 | 48 | sink.releases(selfRef) 49 | 50 | sink 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/syntax/sinkSource/package.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event.syntax 2 | 3 | import org.pico.disposal.std.autoCloseable._ 4 | import org.pico.event.syntax.source._ 5 | import org.pico.event.{Bus, Sink, SinkSource, Source} 6 | 7 | import scala.concurrent.{ExecutionContext, Future} 8 | 9 | package object sinkSource { 10 | implicit class SinkSourceOps_37hUMNo[A, B](val self: SinkSource[A, B]) extends AnyVal { 11 | /** Get a SinkSource that routes a SinkSource via a SinkSource. 12 | */ 13 | def via(that: Bus[B]): SinkSource[A, B] = { 14 | that += self into that 15 | SinkSource.from(self, that) 16 | } 17 | 18 | /** Subscribe the Sink to the SinkSource. 19 | */ 20 | def tap(that: Sink[B]): SinkSource[A, B] = { 21 | that += self into that 22 | self 23 | } 24 | } 25 | 26 | implicit class SinkSourceOps_iY4kPqc[A, B](val self: SinkSource[A, Future[B]]) extends AnyVal { 27 | /** Return a source which emits events whenever the futures from the original 28 | * source complete. Success values will be emitted on the right and failures 29 | * will be emitted on the left. 30 | */ 31 | @deprecated("Use completed instead") 32 | def scheduled(implicit ec: ExecutionContext): Source[Either[Throwable, B]] = completed 33 | 34 | /** Return a source which emits events whenever the futures from the original 35 | * source completes. Success values will be emitted on the right and failures 36 | * will be emitted on the left. 37 | */ 38 | def completed(implicit ec: ExecutionContext): Source[Either[Throwable, B]] = { 39 | val bus = Bus[Either[Throwable, B]] 40 | 41 | bus.disposes(self.asSource.completed into bus) 42 | 43 | bus 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/syntax/source/package.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event.syntax 2 | 3 | import java.io.Closeable 4 | import java.util.concurrent.atomic.AtomicLong 5 | 6 | import cats.kernel.Semigroup 7 | import cats.{Foldable, Monoid} 8 | import org.pico.disposal.std.autoCloseable._ 9 | import org.pico.event._ 10 | import org.pico.event.std.all._ 11 | import org.pico.event.syntax.future._ 12 | 13 | import scala.concurrent.{ExecutionContext, Future} 14 | 15 | package object source { 16 | implicit class SourceOps_hJob2ex[A](val self: Source[A]) extends AnyVal { 17 | /** Create a view with an initial value, which will have the latest value that was 18 | * emitted by the event source. 19 | * 20 | * @param initial The initial value 21 | * @return The view that will change to contain the latest value emitted by the source 22 | */ 23 | @inline 24 | final def latest(initial: A): View[A] = self.foldRight(initial)((v, _) => v) 25 | 26 | /** Create a view that counts the number of events that have been emitted. 27 | * 28 | * @return The view that will change to contain the latest value emitted by the source 29 | */ 30 | @inline 31 | final def eventCount: View[Long] = { 32 | new View[Long] { 33 | val data = new AtomicLong(0L) 34 | 35 | this.disposes(self.subscribe { _ => 36 | data.incrementAndGet() 37 | invalidations.invalidate() 38 | }) 39 | 40 | override def value: Long = data.get() 41 | 42 | override def invalidations: Invalidations = Invalidations() 43 | } 44 | } 45 | 46 | /** Update a cell with events using a combining function 47 | */ 48 | @inline 49 | final def update[B](cell: Cell[B])(f: (A, B) => B): AutoCloseable = { 50 | self.subscribe(a => cell.update(b => f(a, b))) 51 | } 52 | 53 | /** Get a Source that routes a Source via a SinkSource. 54 | */ 55 | @inline 56 | final def via[B](that: SinkSource[A, B]): Source[B] = { 57 | that += self into that 58 | that 59 | } 60 | 61 | /** Subscribe the Sink to the Source. 62 | */ 63 | @inline 64 | final def tap(that: Sink[A]): Source[A] = { 65 | self += self into that 66 | self 67 | } 68 | 69 | /** Fold the event source into a value given the value's initial state. 70 | * 71 | * @param f The folding function 72 | * @param initial The initial state 73 | * @tparam B Type of the new value 74 | * @return The value. 75 | */ 76 | @inline 77 | final def foldLeft[B](initial: B)(f: (B, A) => B): View[B] = { 78 | val cell = Cell[B](initial) 79 | 80 | cell.disposes(self.subscribe(v => cell.update(a => f(a, v)))) 81 | 82 | cell 83 | } 84 | 85 | /** Fold the event source into a cell. 86 | * 87 | * @param f The folding function 88 | * @tparam B Type of the new value 89 | * @return Subscription, which when closed stops updating the cell 90 | */ 91 | @inline 92 | final def foldRightInto[B](cell: Cell[B])(f: (A, => B) => B): Closeable = { 93 | self.subscribe { a => 94 | cell.update(b => f(a, b)) 95 | } 96 | } 97 | 98 | /** Fold the event source into a cell. 99 | * 100 | * @param f The folding function 101 | * @tparam B Type of the new value 102 | * @return Subscription, which when closed stops updating the cell 103 | */ 104 | @inline 105 | final def foldLeftInto[B](cell: Cell[B])(f: (B, A) => B): Closeable = { 106 | self.subscribe { a => 107 | cell.update(b => f(b, a)) 108 | } 109 | } 110 | 111 | /** Fold the event source into a cell using Monoid.combine 112 | * 113 | * @return The view containing folded value. 114 | */ 115 | @inline 116 | final def combined(implicit ev: Monoid[A]): View[A] = { 117 | self.foldLeft(Monoid[A].empty)(Monoid[A].combine) 118 | } 119 | 120 | /** Fold the event source using Semigroup.combine into a cell. 121 | * 122 | * @return Subscription, which when closed stops updating the cell 123 | */ 124 | @inline 125 | final def combinedInto(cell: Cell[A])(implicit ev: Semigroup[A]): Closeable = { 126 | self.subscribe { a => 127 | cell.update(ev.combine(_, a)) 128 | } 129 | } 130 | } 131 | 132 | implicit class SourceOps_KhVNHpu[A, B](val self: Source[Either[A, B]]) extends AnyVal { 133 | /** Divert values on the left of emitted events by the source into the provided sink. 134 | * 135 | * Values on the right of the event will be emitted by the returned source. 136 | * 137 | * @param sink The sink to which left side of emitted events will be published 138 | * @return The source that emits the right side of emitted events 139 | */ 140 | @inline 141 | final def divertLeft(sink: Sink[A]): Source[B] = { 142 | new SimpleBus[B] { temp => 143 | temp += self.subscribe { 144 | case Right(rt) => temp.publish(rt) 145 | case Left(lt) => sink.publish(lt) 146 | } 147 | } 148 | } 149 | 150 | /** Divert values on the right of emitted events by the source into the provided sink. 151 | * 152 | * Values on the left of the event will be emitted by the returned source. 153 | * 154 | * @param sink The sink to which right side of emitted events will be published 155 | * @return The source that emits the left side of emitted events 156 | */ 157 | @inline 158 | final def divertRight(sink: Sink[B]): Source[A] = { 159 | new SimpleBus[A] { temp => 160 | temp += self.subscribe { 161 | case Right(rt) => sink.publish(rt) 162 | case Left(lt) => temp.publish(lt) 163 | } 164 | } 165 | } 166 | 167 | /** New Source propagating left of Either. 168 | */ 169 | @inline 170 | final def justLeft: Source[A] = divertRight(ClosedSink) 171 | 172 | /** New Source propagating right of Either. 173 | */ 174 | @inline 175 | final def justRight: Source[B] = divertLeft(ClosedSink) 176 | } 177 | 178 | implicit class SourceOps_iY4kPqc[A](val self: Source[Future[A]]) extends AnyVal { 179 | /** Return a source which emits events whenever the futures from the original 180 | * source complete. Success values will be emitted on the right and failures 181 | * will be emitted on the left. 182 | */ 183 | @inline 184 | final def completed(implicit ec: ExecutionContext): Source[Either[Throwable, A]] = { 185 | val bus = Bus[Either[Throwable, A]] 186 | 187 | bus.disposes(self.subscribe(_.completeInto(bus))) 188 | 189 | bus 190 | } 191 | } 192 | 193 | implicit class SourceOps_ZWi7qoi[A](val self: Source[Option[A]]) extends AnyVal { 194 | /** Propagate Some value as Right and None as the provided Left value. 195 | */ 196 | @inline 197 | final def right[B](leftValue: => B): Source[Either[B, A]] = self.map { 198 | case Some(v) => Right(v) 199 | case None => Left(leftValue) 200 | } 201 | 202 | /** Propagate Some value as Left and None as the provided Right value. 203 | */ 204 | @inline 205 | final def left[B](rightValue: => B): Source[Either[A, B]] = self.map { 206 | case Some(v) => Left(v) 207 | case None => Right(rightValue) 208 | } 209 | 210 | /** Propagate Some value. 211 | */ 212 | def justSome: Source[A] = self.mapConcat[Option, A](identity) 213 | } 214 | 215 | implicit class SourceOps_r8pPKQJ(val self: Source[Long]) extends AnyVal { 216 | @inline 217 | final def sum: View[Long] = { 218 | val cell = LongCell(0L) 219 | 220 | cell.disposes(self.subscribe(cell.addAndGet(_))) 221 | 222 | cell 223 | } 224 | } 225 | 226 | implicit class SourceOps_iAPaWug(val self: Source[Int]) extends AnyVal { 227 | @inline 228 | final def sum: View[Int] = { 229 | val cell = IntCell(0) 230 | 231 | cell.disposes(self.subscribe(cell.addAndGet(_))) 232 | 233 | cell 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/syntax/sourceLike/package.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event.syntax 2 | 3 | package object sourceLike { 4 | implicit class SourceLikeOps_n9E4Dsd[A](val self: A) extends AnyVal { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /pico-event/src/main/scala/org/pico/event/syntax/writer/package.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event.syntax 2 | 3 | import java.io.Writer 4 | 5 | import org.pico.event.Sink 6 | import org.pico.event.io.NewlineCountWriter 7 | 8 | package object writer { 9 | implicit class WriterOps_vYaV2CB(val self: Writer) extends AnyVal { 10 | /** Decorate a writer with the behaviour of counting newlines as they are written and publishing the counts 11 | * to the provided sink. 12 | */ 13 | @inline final def countNewlinesIn(sink: Sink[Long]): Writer = NewlineCountWriter(self, sink) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pico-event/src/main/tut/tutorial.md: -------------------------------------------------------------------------------- 1 | ## Publish-subscribe pattern 2 | This library implements the publish-subscribe pattern with thread-safety, type-safety and lifetime 3 | management in its simplest form. It relies on the [pico-disposal](https://github.com/pico-works/pico-disposal) 4 | library for managing subscribe lifetimes. 5 | 6 | Included in the library are all the types necessary for components to interact with each other 7 | via the publish-subscribe pattern and a small number of useful combinators for building source 8 | and sink pipelines. 9 | 10 | ## Buses 11 | A `Bus` is an object that events can be published to. When an event is published to it, it will also emit 12 | events. 13 | 14 | ```tut:reset 15 | import org.pico.event._ 16 | 17 | val bus = Bus[Int] 18 | var count = 0 19 | val subscription = bus.subscribe(e => count += e) 20 | bus.publish(1) 21 | assert(count == 1) 22 | ``` 23 | 24 | In the above code, a subscriber function `e => count += e` is registered on the bus. When `1` is published 25 | to the bus on the next line, the subscriber function is called, which adds one to the variable `count`. 26 | 27 | ## Sinks and Sources 28 | The `Bus` is an object that implements two core abstractions of the `pico-event` library: The `Sink` and 29 | the `Source`. 30 | 31 | The `Sink` supports the `publish` method and the `Source` supports the `subscribe` method. 32 | 33 | This is part of the definition of `Sink`: 34 | 35 | ```scala 36 | trait Sink[-A] extends Disposer { self => 37 | def publish(event: A): Unit 38 | ... 39 | } 40 | ``` 41 | 42 | A `Sink[A]` accepts events of type `A` via the `publish` method: 43 | 44 | ```tut:reset 45 | import org.pico.event._ 46 | 47 | val sink: Sink[Int] = ClosedSink 48 | sink.publish(42) 49 | ``` 50 | 51 | This is part of the definition of `Source`: 52 | 53 | ```scala 54 | trait Source[+A] extends Disposer { 55 | def subscribe(subscriber: A => Unit): Closeable 56 | ... 57 | } 58 | ``` 59 | 60 | A `Source[A]` emits events of type `A` to any subscribers. A subscription is created by supplying 61 | a subscriber to the source's `subscribe` method: 62 | 63 | ```tut:reset 64 | import org.pico.event._ 65 | 66 | val source: Source[Int] = ClosedSource 67 | val subscription = source.subscribe(println) 68 | ``` 69 | 70 | By creating a subscription as above, any events emitted by `source` will be printed. 71 | 72 | ## Subscription lifetimes 73 | Subscriptions will continue to be active until either the subscription or source is closed or 74 | disposed, or the subscription is garbage collected. The subscription will maintain a reference to 75 | the source so the source will not be garbage collected until the subscription garbage collector 76 | or the subscription is disposed. 77 | 78 | Both `Source` and `Sink` inherit from `Disposer`: 79 | 80 | ```scala 81 | trait Disposer extends Closeable { 82 | def +=[D: Disposable](disposable: D): D = disposes(disposable) 83 | def disposes[D: Disposable](disposable: D): D 84 | def close(): Unit = disposables.getAndSet(Closed).dispose() 85 | } 86 | ``` 87 | 88 | To dispose a `Source` means all subscriptions listening on it for events will be disposed and no 89 | events will be emitted. 90 | 91 | To dispose a `Sink` means that all calls to `publish` will be ignored. 92 | 93 | The same applies to any type in this library that implements `Closeable`. If they own 94 | subscriptions, then closing those objects will also close those subscriptions. If they maintain 95 | references to resources, then closing those objects will also release those references to 96 | facilitate garbage collection. 97 | 98 | ## Map method for Source 99 | 100 | The `map` method can be used to create one `Source` out of another by supplying a mapping method. 101 | Events emitted by the original `Source` will have the mapping method applied to it with the 102 | result emitted by the new `Source` 103 | 104 | ```tut:reset 105 | import org.pico.event._ 106 | 107 | val stringSource: Source[String] = ClosedSource 108 | val stringLengthSource: Source[Int] = stringSource.map(_.length) 109 | ``` 110 | 111 | ## Effect method for Source 112 | 113 | The `effect` method is similar to map. It behaves much like `source.map(identity)`, i.e. it will 114 | will produce a new source that emits the same events as the original - except that it will also 115 | execute a side-effecting method before doing so. 116 | 117 | ```tut:reset 118 | import org.pico.event._ 119 | 120 | val stringSource: Source[String] = ClosedSource 121 | val printingStringSource: Source[String] = stringSource.effect(println) 122 | ``` 123 | 124 | ## MapConcat method for Source 125 | The `mapConcat` method is similar to `map`, except that the result type of the mapping function 126 | is required to return an `Iterable`. `mapConcat` will then produce a `Source` of the `Iterable` 127 | element type that emits each element of the `Iterable`: 128 | 129 | ```tut:reset 130 | import org.pico.event._ 131 | import org.pico.event.std.all._ 132 | 133 | val source1: Source[List[Int]] = ClosedSource 134 | val source2: Source[Int] = source1.mapConcat(identity) 135 | ``` 136 | 137 | ## Filter method for Source 138 | 139 | The `Sink.filter` method can be used to derived a `Source` that only emits events that satisy a 140 | predicate: 141 | 142 | ```tut:reset 143 | import org.pico.event._ 144 | import org.pico.event.std.all._ 145 | 146 | val source1: Source[Int] = ClosedSource 147 | val source2: Source[Int] = source1.filter(_ % 2 == 0) 148 | ``` 149 | 150 | ## Comap method for Sink 151 | 152 | In a similar fashion, `Sink` has a `comap` method that returns a new `Sink` that transforms events 153 | that are published before with a mapping method. Unlike `map`, which takes the mapping method 154 | `A => B`, the `comap` method takes a mapping method `B => A`. Because `B` is now an argument 155 | type instead of a return type, a type-hint for the argument type must be provided with the `comap` 156 | method as shown below: 157 | 158 | ```tut:reset 159 | import org.pico.event._ 160 | 161 | val stringSink: Sink[Int] = ClosedSink 162 | val stringLengthSink: Sink[String] = stringSink.comap[String](_.length) 163 | ``` 164 | 165 | ## Or method for Source 166 | 167 | The `or` method will merge to sources such that events emitted from the left source will be emitted 168 | in the `Left` case of `Either` and events emitted from the right source will be emitted in the `Right` 169 | case of either: 170 | 171 | ```tut:reset 172 | import org.pico.event._ 173 | 174 | val source1: Source[Int] = ClosedSource 175 | val source2: Source[String] = ClosedSource 176 | val source3: Source[Either[Int, String]] = source1 or source2 177 | ``` 178 | 179 | ## DivertLeft and DivertRight method for Source 180 | A `Source` that has an `Either` element type can divert events that are on one side of the `Either` 181 | into a `Sink`. 182 | 183 | The following diverts the left case: 184 | 185 | ```tut:reset 186 | import org.pico.event._ 187 | import org.pico.event.syntax.source._ 188 | 189 | val source1: Source[Either[Int, String]] = ClosedSource 190 | val sink: Sink[Int] = ClosedSink 191 | val source2: Source[String] = source1.divertLeft(sink) 192 | ``` 193 | The following diverts the right case: 194 | 195 | ```tut:reset 196 | import org.pico.event._ 197 | import org.pico.event.syntax.source._ 198 | 199 | val source1: Source[Either[Int, String]] = ClosedSource 200 | val sink: Sink[String] = ClosedSink 201 | val source2: Source[Int] = source1.divertRight(sink) 202 | ``` 203 | 204 | ## Observable Views 205 | Observable views have the type `View[A]`. They model values that change over time. 206 | 207 | The simplest view that can be constructed is a view that never changes. For example, the following 208 | creates a view with the value 1: 209 | 210 | ```tut:reset 211 | import org.pico.event._ 212 | 213 | val view1 = View(1) 214 | assert(view1.value == 1) 215 | ``` 216 | 217 | A view that changes over time can be constructed from a source. The simplest such view is `latest`, 218 | which constructs a view that maintains a value equal to the latest value emitted by a stream. It 219 | is initialised with an initial value which serves as its value until the source emits a value. 220 | 221 | ```tut:reset 222 | import org.pico.event._ 223 | import org.pico.event.syntax.source._ 224 | 225 | val bus = SinkSource[Int, Int](identity) 226 | val view = bus.asSource.latest(0) 227 | assert(view.value == 0) 228 | bus.publish(2) 229 | assert(view.value == 2) 230 | ``` 231 | 232 | ## Creating obervable views by folding sources 233 | 234 | The `latest` method is actually implemented in terms of another more generic `foldRight` method: 235 | 236 | ```scala 237 | def latest(intial: A): View[A] = this.foldRight(initial)((v, _) => v) 238 | ``` 239 | 240 | The first argument of `foldRight` is the initial value of the resulting view. The second argument 241 | is a function that describes how to combined events with the current value of the view to produce 242 | a new value for the view. 243 | 244 | The implementation of `foldRight` uses optimistic lock-free atomic updates to the value so it is 245 | thread-safe even in situations that the source from which the view is derived emits events in different 246 | threads. 247 | 248 | This form of update will attempt to apply the update function to modify the value optimistically and 249 | atomically. If the update discovers that the value has changed before it could apply the update it 250 | will abort and retry, calling the update function again. This means *it is unsafe to perform any 251 | side-effects in the update function*. Because of the reties, it is also sensible to ensure that 252 | the update function is as fast as possible. 253 | 254 | ## EventCount method for Source 255 | 256 | A `Source` has an `eventCount` method that returns a `View` that counts how many times the original 257 | source has changed values since the `View` was created. 258 | 259 | ```tut:reset 260 | import org.pico.event._ 261 | import org.pico.event.syntax.source._ 262 | 263 | val bus = SinkSource[Int, Int](identity) 264 | val eventCount: View[Long] = bus.eventCount 265 | ``` 266 | 267 | ## Observable Cells 268 | Observable variables have the type `Cell[A]`. They can be created by calling the 269 | companion object constructor method with an initial value. The cell can then be 270 | updated via its `value` field: 271 | 272 | ```tut:reset 273 | import org.pico.event._ 274 | 275 | val counter = Cell[Long](0) 276 | counter.value = 1 277 | ``` 278 | 279 | It has semantics much like the `AtomicReference` type in the standard Java library, 280 | so you can perform the same kinds of atomic operations like the following: 281 | 282 | ```tut:reset 283 | import org.pico.event._ 284 | 285 | val counter = Cell[Long](0) 286 | counter.getAndSet(1) 287 | counter.update(_ + 1) 288 | val success = counter.compareAndSet(1, 2) 289 | ``` 290 | 291 | Moreover, it exposes an event source that emits the new value after every value 292 | change. The following code prints every change to `counter`: 293 | 294 | ```tut:reset 295 | import org.pico.event._ 296 | 297 | val counter = Cell[Long](0) 298 | val source: Source[Long] = counter.source 299 | val subscription = source.subscribe(println) 300 | ``` 301 | 302 | A `Cell` is also a `View` and inherit all its methods. 303 | -------------------------------------------------------------------------------- /pico-event/src/test/scala/org/pico/event/BusSpec.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import java.util.concurrent.atomic.AtomicInteger 4 | 5 | import org.pico.disposal.Disposer 6 | import org.pico.disposal.std.autoCloseable._ 7 | import org.pico.disposal.syntax.disposable._ 8 | import org.pico.event.syntax.source._ 9 | import org.pico.event.syntax.hasForeach._ 10 | import org.pico.event.std.all._ 11 | import org.specs2.mutable.Specification 12 | 13 | class BusSpec extends Specification { 14 | "Bus" should { 15 | "have map method that invokes function one for every publish when there is one subscriber" in { 16 | val bus = Bus[Int] 17 | val counter = new AtomicInteger(0) 18 | val source = bus.map{e => counter.addAndGet(e); e} 19 | val disposer = Disposer() 20 | disposer += source.subscribe(e => ()) 21 | System.gc() 22 | 23 | bus.publish(1) 24 | bus.publish(1) 25 | bus.publish(1) 26 | 27 | counter.get() must_=== 3 28 | } 29 | 30 | "have map method that invokes function one for every publish when there are two subscribers" in { 31 | val bus = Bus[Int] 32 | val counter = new AtomicInteger(0) 33 | val source = bus.map{e => counter.addAndGet(e); e} 34 | val disposer = Disposer() 35 | disposer += source.subscribe(e => ()) 36 | disposer += source.subscribe(e => ()) 37 | System.gc() 38 | 39 | bus.publish(1) 40 | bus.publish(1) 41 | bus.publish(1) 42 | 43 | counter.get() must_=== 3 44 | } 45 | 46 | "should implement map" ! { 47 | val disposer = Disposer() 48 | val es = Bus[Int] 49 | val fs = es.map(_ + 1) 50 | var esValues = List.empty[Int] 51 | var fsValues = List.empty[Int] 52 | disposer += es.subscribe(v => esValues ::= v) 53 | disposer += fs.subscribe(v => fsValues ::= v) 54 | System.gc() 55 | es.publish(10) 56 | esValues must_=== List(10) 57 | fsValues must_=== List(11) 58 | System.gc() 59 | es.publish(20) 60 | esValues must_=== List(20, 10) 61 | fsValues must_=== List(21, 11) 62 | } 63 | 64 | "should be able to mapConcat using an iterable producing function to create a source that emits elements" ! { 65 | val aBus = Bus[(Int, String)] 66 | val bBus = Bus[String] 67 | 68 | val disposer = Disposer() 69 | 70 | disposer += aBus.mapConcat { case (n, s) => List.fill(n)(s) } into bBus 71 | 72 | val result = bBus.foldRight(List.empty[String])(_ :: _) 73 | System.gc() 74 | 75 | aBus.publish(2 -> "A") 76 | aBus.publish(1 -> "B") 77 | aBus.publish(3 -> "C") 78 | 79 | result.value must_=== List("C", "C", "C", "B", "A", "A") 80 | } 81 | 82 | "should be able to divert left of either into sink" ! { 83 | val inBus = Bus[Either[String, Int]] 84 | val ltBus = Bus[String] 85 | val rtBus = Bus[Int] 86 | 87 | val disposer = Disposer() 88 | 89 | disposer += inBus.divertLeft(ltBus).into(rtBus) 90 | 91 | val ltValue = ltBus.foldRight(List.empty[String])(_ :: _) 92 | val rtValue = rtBus.foldRight(List.empty[Int])(_ :: _) 93 | System.gc() 94 | 95 | inBus.publish(Left("A")) 96 | inBus.publish(Right(1)) 97 | inBus.publish(Left("B")) 98 | inBus.publish(Right(2)) 99 | 100 | ltValue.value must_=== List("B", "A") 101 | rtValue.value must_=== List(2, 1) 102 | } 103 | 104 | "should be able to divert right of either into sink" ! { 105 | val inBus = Bus[Either[String, Int]] 106 | val ltBus = Bus[String] 107 | val rtBus = Bus[Int] 108 | 109 | val disposer = Disposer() 110 | 111 | disposer += inBus.divertRight(rtBus).into(ltBus) 112 | 113 | val ltValue = ltBus.foldRight(List.empty[String])(_ :: _) 114 | val rtValue = rtBus.foldRight(List.empty[Int])(_ :: _) 115 | System.gc() 116 | 117 | inBus.publish(Left("A")) 118 | inBus.publish(Right(1)) 119 | inBus.publish(Left("B")) 120 | inBus.publish(Right(2)) 121 | 122 | ltValue.value must_=== List("B", "A") 123 | rtValue.value must_=== List(2, 1) 124 | } 125 | 126 | "should implement into method that creates a subscription on source that writes to sink" ! { 127 | val aBus = Bus[Int] 128 | val bBus = Bus[Int] 129 | val cBus = Bus[Int] 130 | 131 | val disposer = Disposer() 132 | disposer += aBus into bBus 133 | disposer += bBus into cBus 134 | val result = cBus.foldRight(List.empty[Int])(_ :: _) 135 | System.gc() 136 | 137 | aBus.publish(1) 138 | aBus.publish(2) 139 | 140 | result.value must_=== List(2, 1) 141 | 142 | disposer.close() 143 | 144 | aBus.publish(3) 145 | bBus.publish(4) 146 | 147 | result.value must_=== List(2, 1) 148 | } 149 | 150 | "should implement merge method such that merged source emits the same events as both underlying sources" ! { 151 | val ltBus = Bus[Int] 152 | val rtBus = Bus[Int] 153 | val combinedBus = ltBus merge rtBus 154 | val result = combinedBus.foldRight(List.empty[Int])(_ :: _) 155 | System.gc() 156 | ltBus.publish(0) 157 | rtBus.publish(1) 158 | ltBus.publish(2) 159 | rtBus.publish(3) 160 | result.value must_=== List(3, 2, 1, 0) 161 | } 162 | 163 | "Disposed source should no longer emit events" ! { 164 | val bus = Bus[Int] 165 | val source = bus.map(_ * 100) 166 | val result1 = source.foldRight(List.empty[Int])(_ :: _) 167 | bus.publish(0) 168 | source.dispose() 169 | bus.publish(1) 170 | val result2 = source.foldRight(List.empty[Int])(_ :: _) 171 | System.gc() 172 | bus.publish(2) 173 | result1.value must_=== List(0) 174 | result2.value must_=== List() 175 | } 176 | 177 | "Disposed sink should no longer publish events" ! { 178 | val bus = Bus[Int] 179 | val sink: Sink[Int] = bus.comap(_ * 100) 180 | val result1 = bus.foldRight(List.empty[Int])(_ :: _) 181 | System.gc() 182 | sink.publish(0) 183 | sink.dispose() 184 | sink.publish(1) 185 | result1.value must_=== List(0) 186 | } 187 | 188 | "Disposed bus should no longer emit or publish events" ! { 189 | val bus = Bus[Int] 190 | val result = bus.foldRight(List.empty[Int])(_ :: _) 191 | System.gc() 192 | bus.publish(0) 193 | bus.dispose() 194 | bus.publish(1) 195 | result.value must_=== List(0) 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /pico-event/src/test/scala/org/pico/event/CellSpec.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import org.pico.disposal.Disposer 4 | import org.pico.disposal.std.autoCloseable._ 5 | import org.pico.disposal.syntax.disposable._ 6 | import org.pico.event.syntax.disposer._ 7 | import org.specs2.mutable.Specification 8 | 9 | class CellSpec extends Specification { 10 | "Var" should { 11 | // "have map operation" in { 12 | // val cell1 = Cell(1) 13 | // val view1 = cell1.asView.map(_ * 10) 14 | // System.gc() 15 | // 16 | // view1.value must_=== 10 17 | // cell1.value = 2 18 | // view1.value must_=== 20 19 | // cell1.value = 3 20 | // view1.value must_=== 30 21 | // } 22 | 23 | // "have flatMap operation" in { 24 | // val cell1 = Cell(0) 25 | // val cell2 = Cell(0) 26 | // 27 | // val result = for { 28 | // a <- cell1.asView 29 | // b <- cell2.asView 30 | // } yield a + b 31 | // 32 | // System.gc() 33 | // 34 | // result.value must_=== 0 35 | // cell1.value = 2 36 | // result.value must_=== 2 37 | // cell2.value = 3 38 | // result.value must_=== 5 39 | // cell1.value = 5 40 | // result.value must_=== 8 41 | // } 42 | 43 | // "have applyIn operation on two arguments" in { 44 | // val cell1 = Cell[Int](0) 45 | // val view1 = cell1.asView.map(_ + 1) 46 | // 47 | // System.gc() 48 | // 49 | // view1.value must_=== 1 50 | // cell1.value = 1 51 | // view1.value must_=== 2 52 | // cell1.value = 2 53 | // view1.value must_=== 3 54 | // cell1.value = 3 55 | // view1.value must_=== 4 56 | // } 57 | 58 | // "have applyIn operation on two arguments" in { 59 | // val cell1 = Cell[Int](0) 60 | // val view1 = cell1.asView.map(_ + 1) 61 | // 62 | // System.gc() 63 | // 64 | // cell1.value = 1 65 | // view1.value must_=== 2 66 | // } 67 | 68 | "have a source that can be folded" in { 69 | val cell1 = Cell(0) 70 | val view1 = cell1.asView 71 | System.gc() 72 | cell1.value = 1 73 | view1.value must_=== 1 74 | } 75 | 76 | // "have applyIn operation on two arguments" in { 77 | // val cell1 = Cell[Int => Int](identity) 78 | // val cell2 = Cell[Int](0) 79 | // 80 | // val result = cell1.asView ap cell2.asView 81 | // System.gc() 82 | // 83 | // result.value must_=== 0 84 | // cell2.value = 1 85 | // result.value must_=== 1 86 | // cell1.value = _ + 10 87 | // result.value must_=== 11 88 | // cell2.value = 2 89 | // result.value must_=== 12 90 | // cell1.value = _ + 20 91 | // result.value must_=== 22 92 | // } 93 | 94 | // "have applyIn operation on two arguments" in { 95 | // val cell1 = Cell[Int](0) 96 | // val cell2 = Cell[Int](0) 97 | // 98 | // val result = cell1.asView.map[Int => Int](x => y => x + y) ap cell2.asView 99 | // System.gc() 100 | // 101 | // result.value must_=== 0 102 | // cell1.value = 2 103 | // result.value must_=== 2 104 | // cell2.value = 3 105 | // result.value must_=== 5 106 | // cell1.value = 5 107 | // result.value must_=== 8 108 | // } 109 | 110 | // "have applyIn operation on three arguments" in { 111 | // val cell1 = Cell(0) 112 | // val cell2 = Cell(0) 113 | // val cell3 = Cell(0) 114 | // 115 | // val result = (cell1.asView |@| cell2.asView |@| cell3.asView).map(_ + _ + _) 116 | // System.gc() 117 | // 118 | // result.value must_=== 0 119 | // cell1.value = 2 120 | // result.value must_=== 2 121 | // cell2.value = 3 122 | // result.value must_=== 5 123 | // cell1.value = 5 124 | // result.value must_=== 8 125 | // } 126 | 127 | // "have applyIn operation on three arguments" in { 128 | // val cell1 = Cell(0) 129 | // val cell2 = Cell(0) 130 | // val cell3 = Cell(0) 131 | // val cell4 = Cell(0) 132 | // 133 | // val result = (cell1.asView |@| cell2.asView |@| cell3.asView |@| cell4.asView).map(_ + _ + _ + _) 134 | // System.gc() 135 | // 136 | // result.value must_=== 0 137 | // cell1.value = 2 138 | // result.value must_=== 2 139 | // cell2.value = 3 140 | // result.value must_=== 5 141 | // cell1.value = 5 142 | // result.value must_=== 8 143 | // } 144 | 145 | "be able to be reset by Disposer" in { 146 | val disposer = Disposer() 147 | val view1 = disposer.resets(0, Cell(1)) 148 | 149 | view1.value must_=== 1 150 | view1.value = 2 151 | view1.value must_=== 2 152 | disposer.dispose() 153 | view1.value must_=== 0 154 | } 155 | 156 | "be able to be able to getAndSet" in { 157 | val cell1 = Cell(1) 158 | val view1 = cell1.asView 159 | 160 | view1.value must_=== 1 161 | cell1.getAndSet(2) must_=== 1 162 | cell1.value must_=== 2 163 | view1.value must_=== 2 164 | } 165 | 166 | "be able to be able to compareAndSet" in { 167 | val cell1 = Cell(1) 168 | val view1 = cell1.asView 169 | 170 | cell1.compareAndSet(1, 2) must_=== true 171 | cell1.value must_=== 2 172 | view1.value must_=== 2 173 | 174 | cell1.compareAndSet(1, 3) must_=== false 175 | cell1.value must_=== 2 176 | view1.value must_=== 2 177 | } 178 | 179 | "be able to be able to update" in { 180 | val cell1 = Cell(1) 181 | val view1 = cell1.asView 182 | 183 | cell1.update(_ + 9) must_=== (1, 10) 184 | cell1.value must_=== 10 185 | view1.value must_=== 10 186 | } 187 | 188 | "be able to be able to update conditionally" in { 189 | val cell1 = Cell(1) 190 | val view1 = cell1.asView 191 | 192 | cell1.updateIf(_ > 5, _ + 9) must_=== None 193 | cell1.value must_=== 1 194 | view1.value must_=== 1 195 | 196 | cell1.updateIf(_ < 5, _ + 9) must_=== Some(1 -> 10) 197 | cell1.value must_=== 10 198 | view1.value must_=== 10 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /pico-event/src/test/scala/org/pico/event/ClosedSinkSpec.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import org.specs2.mutable.Specification 4 | 5 | class ClosedSinkSpec extends Specification { 6 | "ClosedSink" should { 7 | "allow publish of any type" in { 8 | ClosedSink.publish(()) 9 | ClosedSink.publish(1) 10 | ClosedSink.publish(true) 11 | ok 12 | } 13 | 14 | "implement comap that returns self" in { 15 | ClosedSink.comap[Any](identity) must_=== ClosedSink 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pico-event/src/test/scala/org/pico/event/ClosedSourceSpec.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import org.pico.disposal.std.autoCloseable._ 4 | import org.pico.disposal.{Closed, Disposer} 5 | import org.pico.event.std.all._ 6 | import org.pico.event.syntax.source._ 7 | import org.specs2.mutable.Specification 8 | 9 | class ClosedSourceSpec extends Specification { 10 | "ClosedSource" should { 11 | "allow publish of any type" in { 12 | ClosedSource.subscribe(_ => ()) must_=== Closed 13 | } 14 | 15 | "implement map that returns ClosedSource" in { 16 | ClosedSource.map(identity) must_=== ClosedSource 17 | } 18 | 19 | "implement effect that returns ClosedSource" in { 20 | ClosedSource.effect(identity) must_=== ClosedSource 21 | } 22 | 23 | "implement mapConcat that returns ClosedSource" in { 24 | ClosedSource.mapConcat(_ => Iterable.empty) must_=== ClosedSource 25 | } 26 | 27 | "implement merge that returns source that emits same events as `that`" in { 28 | val bus = Bus[Int] 29 | val source = ClosedSource.merge(bus) 30 | val view = source.latest(0) 31 | 32 | bus.publish(1) 33 | view.value must_=== 1 34 | } 35 | 36 | "implement foldRight that returns source that emits same events as `that`" in { 37 | val view = ClosedSource.foldRight(1)((_, b) => b) 38 | view.value must_=== 1 39 | } 40 | 41 | "implement foldRight that returns a view that never changes" in { 42 | val bus = Bus[Int] 43 | val view = bus.latest(0) 44 | ClosedSource.into(bus) 45 | view.value must_=== 0 46 | } 47 | 48 | "implement filter that returns ClosedSource" in { 49 | ClosedSource.filter(_ => true) must_=== ClosedSource 50 | } 51 | 52 | "implement or that returns source that emits same events as `that`" in { 53 | val bus = Bus[Int] 54 | val source = ClosedSource or bus 55 | val leftBus = Bus[Any] 56 | val rightView = source.divertLeft(leftBus).latest(0) 57 | val count = leftBus.eventCount 58 | val disposer = Disposer() 59 | disposer += leftBus.subscribe(_ => println("Hello")) 60 | bus.publish(1) 61 | bus.publish(10) 62 | rightView.value must_=== 10 63 | count.value must_=== 0 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /pico-event/src/test/scala/org/pico/event/SinkSpec.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import org.pico.event.syntax.sink._ 4 | import org.specs2.mutable.Specification 5 | import org.pico.disposal.std.autoCloseable._ 6 | import org.pico.disposal.syntax.disposable._ 7 | 8 | class SinkSpec extends Specification { 9 | "Sink" should { 10 | "have filter operation" in { 11 | val bus = Bus[Int] 12 | val result = bus.foldRight(List.empty[Int])(_ :: _) 13 | val sink = bus.cofilter(_ % 2 == 0) 14 | result.value must_=== List.empty 15 | sink.publish(1) 16 | sink.publish(2) 17 | sink.publish(3) 18 | result.value must_=== List(2) 19 | sink.dispose() 20 | sink.publish(4) 21 | result.value must_=== List(2) 22 | } 23 | 24 | "have comap operation" in { 25 | val bus = Bus[Int] 26 | val result = bus.foldRight(List.empty[Int])(_ :: _) 27 | val sink = bus.comap[String](_.length) 28 | result.value must_=== List.empty 29 | sink.publish("1") 30 | sink.publish("2") 31 | sink.publish("3") 32 | result.value must_=== List(1, 1, 1) 33 | sink.dispose() 34 | sink.publish("4") 35 | result.value must_=== List(1, 1, 1) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pico-event/src/test/scala/org/pico/event/SourceSpec.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import org.pico.disposal.Disposer 4 | import org.pico.disposal.std.autoCloseable._ 5 | import org.pico.disposal.syntax.disposable._ 6 | import org.pico.event.syntax.source._ 7 | import org.pico.event.syntax.sinkSource._ 8 | import org.specs2.mutable.Specification 9 | 10 | class SourceSpec extends Specification { 11 | "Source" should { 12 | "have filter operation" in { 13 | val bus = Bus[Int] 14 | val source = bus.filter(_ % 2 == 0) 15 | val result = source.foldRight(List.empty[Int])(_ :: _) 16 | System.gc() 17 | result.value must_=== List.empty 18 | bus.publish(1) 19 | bus.publish(2) 20 | bus.publish(3) 21 | result.value must_=== List(2) 22 | bus.dispose() 23 | bus.publish(4) 24 | result.value must_=== List(2) 25 | } 26 | 27 | "have map operation" in { 28 | val bus = Bus[Int] 29 | val source = bus.map(_ * 10) 30 | val result = source.foldRight(List.empty[Int])(_ :: _) 31 | System.gc() 32 | 33 | bus.publish(1) 34 | result.value must_=== List(10) 35 | bus.publish(2) 36 | result.value must_=== List(20, 10) 37 | } 38 | 39 | "have or operation" in { 40 | val bus1 = Bus[Int] 41 | val bus2 = Bus[String] 42 | val source1: Source[Int] = bus1 43 | val source2: Source[String] = bus2 44 | val source3: Source[Either[Int, String]] = source1 or source2 45 | val result = source3.foldRight(List.empty[Either[Int, String]])(_ :: _) 46 | System.gc() 47 | 48 | bus1.publish(1) 49 | bus2.publish("Hello") 50 | 51 | result.value must_=== List(Right("Hello"), Left(1)) 52 | } 53 | 54 | "have count operation" in { 55 | val bus = Bus[Int] 56 | val count = bus.eventCount 57 | System.gc() 58 | 59 | count.value must_=== 0 60 | bus.publish(1) 61 | count.value must_=== 1 62 | bus.publish(1) 63 | count.value must_=== 2 64 | } 65 | 66 | "have effect operation" in { 67 | val bus = Bus[Int] 68 | val disposer = Disposer() 69 | var count = 0 70 | disposer += bus.effect(e => count += e) 71 | System.gc() 72 | 73 | count must_=== 0 74 | bus.publish(1) 75 | count must_=== 1 76 | bus.publish(2) 77 | count must_=== 3 78 | } 79 | 80 | "allow consecutive map operations" in { 81 | val bus = Bus[Int] 82 | val source = bus.map(_ * 100).map(_ + 10) 83 | val v = source.latest(0) 84 | System.gc() 85 | 86 | v.value must_=== 0 87 | bus.publish(1) 88 | v.value must_=== 110 89 | bus.publish(2) 90 | v.value must_=== 210 91 | } 92 | 93 | "have viaBus method taking a SinkSource" in { 94 | val bus1 = Bus[Int] 95 | val bus2 = Bus[Int] 96 | val bus3: SinkSource[Int, Int] = bus1.via(bus2) 97 | val result = bus3.foldRight(List.empty[Int])(_ :: _) 98 | 99 | bus1.publish(1) 100 | 101 | result.value must_=== List(1) 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /pico-event/src/test/scala/org/pico/event/ViewSpec.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event 2 | 3 | import org.pico.event.syntax.source._ 4 | import org.specs2.mutable.Specification 5 | import cats.syntax.applicative._ 6 | import cats.syntax.functor._ 7 | import cats.syntax.apply._ 8 | import cats.syntax.cartesian._ 9 | 10 | class ViewSpec extends Specification { 11 | "View" should { 12 | "have apply method that creates constant" in { 13 | val view = View(1) 14 | view.value must_=== 1 15 | } 16 | 17 | "have map operation" in { 18 | val bus = Bus[Int] 19 | val view1 = bus.latest(1) 20 | val view2 = view1.map(_ * 10) 21 | System.gc() 22 | 23 | view1.value must_=== 1 24 | view2.value must_=== 10 25 | 26 | bus.publish(2) 27 | view1.value must_=== 2 28 | view2.value must_=== 20 29 | 30 | bus.publish(3) 31 | view1.value must_=== 3 32 | view2.value must_=== 30 33 | } 34 | 35 | // "have flatMap operation" in { 36 | // val bus1 = Bus[Int] 37 | // val bus2 = Bus[Int] 38 | // val view1 = bus1.latest(0) 39 | // val view2 = bus2.latest(0) 40 | // 41 | // val result = for { 42 | // a <- view1 43 | // b <- view2 44 | // } yield a + b 45 | // 46 | // System.gc() 47 | // 48 | // result.value must_=== 0 49 | // bus1.publish(2) 50 | // result.value must_=== 2 51 | // bus2.publish(3) 52 | // result.value must_=== 5 53 | // bus1.publish(5) 54 | // result.value must_=== 8 55 | // } 56 | 57 | "have applyIn operation on two arguments" in { 58 | val cell1 = Cell[Int](0) 59 | val view1 = cell1.asView.map(_ + 1) 60 | 61 | System.gc() 62 | 63 | view1.value must_=== 1 64 | cell1.value = 1 65 | view1.value must_=== 2 66 | cell1.value = 2 67 | view1.value must_=== 3 68 | cell1.value = 3 69 | view1.value must_=== 4 70 | } 71 | 72 | "have applyIn operation on two arguments" in { 73 | val cell1 = Cell[Int](0) 74 | val view1 = cell1.asView.map(_ + 1) 75 | 76 | System.gc() 77 | 78 | cell1.value = 1 79 | view1.value must_=== 2 80 | } 81 | 82 | "have a source that can be folded" in { 83 | val cell1 = Cell(0) 84 | val view1 = cell1.asView 85 | cell1.value = 1 86 | view1.value must_=== 1 87 | cell1.value = 2 88 | view1.value must_=== 2 89 | } 90 | 91 | "have applyIn operation on two arguments" in { 92 | val cell1 = Cell[Int => Int](identity) 93 | val cell2 = Cell[Int](0) 94 | val view1 = cell1.asView 95 | val view2 = cell2.asView 96 | 97 | val result = view1 ap view2 98 | System.gc() 99 | 100 | result.value must_=== 0 101 | cell2.value = 1 102 | result.value must_=== 1 103 | cell1.value = _ + 10 104 | result.value must_=== 11 105 | cell2.value = 2 106 | result.value must_=== 12 107 | cell1.value = _ + 20 108 | result.value must_=== 22 109 | } 110 | 111 | "have applyIn operation on three arguments" in { 112 | val bus1 = Bus[Int] 113 | val bus2 = Bus[Int] 114 | val bus3 = Bus[Int] 115 | val view1 = bus1.latest(0) 116 | val view2 = bus2.latest(0) 117 | val view3 = bus3.latest(0) 118 | 119 | val result = (view1 |@| view2 |@| view3).map(_ + _ + _) 120 | System.gc() 121 | 122 | result.value must_=== 0 123 | bus1.publish(2) 124 | result.value must_=== 2 125 | bus2.publish(3) 126 | result.value must_=== 5 127 | bus1.publish(5) 128 | result.value must_=== 8 129 | } 130 | 131 | "have applyIn operation on three arguments" in { 132 | val bus1 = Bus[Int] 133 | val bus2 = Bus[Int] 134 | val bus3 = Bus[Int] 135 | val bus4 = Bus[Int] 136 | val view1 = bus1.latest(0) 137 | val view2 = bus2.latest(0) 138 | val view3 = bus3.latest(0) 139 | val view4 = bus4.latest(0) 140 | 141 | val result = (view1 |@| view2 |@| view3 |@| view4).map(_ + _ + _ + _) 142 | System.gc() 143 | 144 | result.value must_=== 0 145 | bus1.publish(2) 146 | result.value must_=== 2 147 | bus2.publish(3) 148 | result.value must_=== 5 149 | bus1.publish(5) 150 | result.value must_=== 8 151 | } 152 | 153 | "have asView method" in { 154 | val view = View(1) 155 | view.value must_== view.asView.value 156 | } 157 | 158 | "have point syntax" in { 159 | 1.pure[View].value must_=== 1 160 | } 161 | 162 | "Should run quickly" in { 163 | val bus = Bus[Long] 164 | val count = bus.eventCount 165 | 166 | val before = System.currentTimeMillis() 167 | 168 | val threads = (0L until 10L).map { threadId => 169 | val thread = new Thread { 170 | override def run(): Unit = { 171 | for (j <- 0L until 100000L) { 172 | bus.publish(1) 173 | } 174 | } 175 | } 176 | 177 | thread.start() 178 | 179 | thread 180 | } 181 | 182 | threads.foreach(_.join) 183 | 184 | val after = System.currentTimeMillis() 185 | 186 | val time = after - before 187 | 188 | ok 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /pico-event/src/test/scala/org/pico/event/concurrent/ExecutionContextBusSpec.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event.concurrent 2 | 3 | import java.util.concurrent.atomic.AtomicInteger 4 | import java.util.concurrent.{Semaphore, TimeUnit} 5 | 6 | import org.pico.disposal.Auto 7 | import org.pico.disposal.std.autoCloseable._ 8 | import org.pico.event.Bus 9 | import org.pico.event.syntax.sinkSource._ 10 | import org.specs2.mutable.Specification 11 | 12 | import scala.concurrent.ExecutionContext.Implicits.global 13 | 14 | class ExecutionContextBusSpec extends Specification { 15 | "Can use ExecutionContextBus" in { 16 | val semaphore = new Semaphore(0) 17 | val valueRef = new AtomicInteger(0) 18 | 19 | for { 20 | bus1 <- Auto(Bus[Int]) 21 | bus2 <- Auto(bus1.via(ExecutionContextBus[Int])) 22 | _ <- Auto { 23 | bus2.subscribe { v => 24 | valueRef.set(v) 25 | semaphore.release() 26 | } 27 | } 28 | } { 29 | bus1.publish(1) 30 | semaphore.tryAcquire(1, TimeUnit.SECONDS) 31 | valueRef.get ==== 1 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pico-event/src/test/scala/org/pico/event/performance/ViewSpec.scala: -------------------------------------------------------------------------------- 1 | package org.pico.event.performance 2 | 3 | import org.pico.event._ 4 | import org.specs2.mutable.Specification 5 | 6 | class ViewSpec extends Specification { 7 | "View" should { 8 | "have performant eventCount method" in { 9 | val bus = Bus[Long] 10 | 11 | val threads = (0L until 10L).map { threadId => 12 | val thread = new Thread { 13 | override def run(): Unit = { 14 | for (j <- 0L until 100000L) { 15 | bus.publish(1) 16 | } 17 | } 18 | } 19 | 20 | thread.start() 21 | 22 | thread 23 | } 24 | 25 | threads.foreach(_.join()) 26 | 27 | threads.foreach { thread => 28 | thread.getState must_=== Thread.State.TERMINATED 29 | } 30 | 31 | ok 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pico-fake/src/main/scala/org/pico/fake/Fake.scala: -------------------------------------------------------------------------------- 1 | package org.pico.fake 2 | 3 | object Fake { 4 | def touch(): Unit = () 5 | } 6 | -------------------------------------------------------------------------------- /pico-fake/src/test/scala/org/pico/fake/FakeSpec.scala: -------------------------------------------------------------------------------- 1 | package org.pico.fake 2 | 3 | import org.specs2.mutable.Specification 4 | 5 | class FakeSpec extends Specification { 6 | "Fake" in { 7 | Fake.touch() 8 | success 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /project/Build.scala: -------------------------------------------------------------------------------- 1 | import sbt.Keys._ 2 | import sbt._ 3 | import tut.Plugin.tutSettings 4 | 5 | object Build extends sbt.Build { 6 | val pico_atomic = "org.pico" %% "pico-atomic" % "0.3.1" 7 | val pico_disposal = "org.pico" %% "pico-disposal" % "1.0.10" 8 | val cats_core = "org.typelevel" %% "cats-core" % "0.9.0" 9 | 10 | val specs2_core = "org.specs2" %% "specs2-core" % "3.8.6" 11 | 12 | def env(name: String): Option[String] = Option(System.getenv(name)) 13 | 14 | implicit class ProjectOps(self: Project) { 15 | def standard(theDescription: String) = { 16 | self 17 | .settings(scalacOptions in Test ++= Seq("-Yrangepos")) 18 | .settings(publishTo := env("PICO_PUBLISH_TO").map("Releases" at _)) 19 | .settings(description := theDescription) 20 | .settings(isSnapshot := true) 21 | .settings(resolvers += Resolver.sonatypeRepo("releases")) 22 | .settings(addCompilerPlugin("org.spire-math" % "kind-projector" % "0.9.3" cross CrossVersion.binary)) 23 | .settings(tutSettings: _*) 24 | } 25 | 26 | def notPublished = self.settings(publish := {}).settings(publishArtifact := false) 27 | 28 | def libs(modules: ModuleID*) = self.settings(libraryDependencies ++= modules) 29 | 30 | def testLibs(modules: ModuleID*) = self.libs(modules.map(_ % "test"): _*) 31 | } 32 | 33 | lazy val `pico-fake` = Project(id = "pico-fake", base = file("pico-fake")) 34 | .standard("Fake project").notPublished 35 | .testLibs(specs2_core) 36 | 37 | lazy val `pico-event` = Project(id = "pico-event", base = file("pico-event")) 38 | .standard("Tiny publish-subscriber library") 39 | .libs(pico_atomic, pico_disposal, cats_core) 40 | .testLibs(specs2_core) 41 | 42 | lazy val all = Project(id = "pico-event-project", base = file(".")) 43 | .notPublished 44 | .aggregate(`pico-event`, `pico-fake`) 45 | } 46 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.12 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += "Typesafe Repository" at "https://repo.typesafe.com/typesafe/releases/" 2 | 3 | addSbtPlugin("com.codacy" % "sbt-codacy-coverage" % "1.3.0") 4 | addSbtPlugin("com.frugalmechanic" % "fm-sbt-s3-resolver" % "0.9.0") 5 | addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-M14") 6 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.4.0") 7 | addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.4.6") 8 | -------------------------------------------------------------------------------- /scripts/check-env-variables.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | declare -a vars=(\ 4 | "AWS_ACCESS_KEY_ID Access key for publishing to S3" \ 5 | "AWS_SECRET_ACCESS_KEY Secret key for publishing to S3" \ 6 | ) 7 | 8 | for var in "${vars[@]}"; do 9 | v=${var%% *} 10 | if [ "${!v-x}" = "x" -a "${!v-y}" = "y" ]; then 11 | printf 'NOT FOUND: $%s\n' "$var" 12 | _not_found=true 13 | else 14 | printf 'found: $%s\n' "$v" 15 | fi 16 | done 17 | 18 | [ -z "${_not_found}" ] && exit 0 19 | 20 | echo "*****************************" 21 | echo "MISSING ENVIRONMENT VARIABLES" 22 | echo "*****************************" 23 | exit 1 24 | -------------------------------------------------------------------------------- /version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | _sem_ver="$(cat version.txt | xargs)" 4 | _commits="$(git rev-list --count HEAD)" 5 | _hash="$(git rev-parse --short HEAD)" 6 | 7 | if [ $# -eq 0 ]; then 8 | if [ "${CIRCLE_PROJECT_USERNAME:-}" = "pico-works" ]; then 9 | if [ "${CIRCLE_TAG:-}" = "v${_sem_ver}" ]; then 10 | echo "$_sem_ver" 11 | exit 0 12 | fi 13 | 14 | if [ "${CIRCLE_BRANCH:-}" = "develop" ]; then 15 | echo "$_sem_ver-$_commits" 16 | exit 0 17 | fi 18 | fi 19 | 20 | echo "$_sem_ver-$_commits-$_hash" 21 | else 22 | case "$1" in 23 | tag) 24 | _untagged_deps="$(cat project/Build.scala | grep val | grep 'org.pico' | grep '[0-9]-[0-9]')" 25 | 26 | if [ "$(git rev-parse --abbrev-ref HEAD)" != "develop" ]; then 27 | echo "Cannot tag from branch other than develop. To force, type:" 28 | echo "" 29 | echo " git tag -a \"v${_sem_ver}\" -m \"New version ${_sem_ver}\" -f" 30 | echo " git push up "v${_sem_ver}" -f" 31 | echo "" 32 | exit 1 33 | fi 34 | 35 | git fetch --all 36 | 37 | _local=$(git rev-parse @) 38 | _remote=$(git rev-parse up/develop) 39 | 40 | if [ "$_local" != "$_remote" ]; then 41 | echo "Cannot tag from out-of-date develop. To force, type:" 42 | echo "" 43 | echo " git tag -a \"v${_sem_ver}\" -m \"New version ${_sem_ver}\" -f" 44 | echo " git push up "v${_sem_ver}" -f" 45 | echo "" 46 | exit 1 47 | fi 48 | 49 | if [ "$(git rev-parse --abbrev-ref HEAD)" != "develop" ]; then 50 | echo "Must be on develop branch to tag" 51 | exit 1 52 | fi 53 | 54 | if [ ! -z "$_untagged_deps" ]; then 55 | echo "Cannot tag because untagged dependencies exist. To force, type:" 56 | echo "" 57 | echo " git tag -a \"v${_sem_ver}\" -m \"New version ${_sem_ver}\" -f" 58 | echo " git push up "v${_sem_ver}" -f" 59 | echo "" 60 | echo "$_untagged_deps" 61 | echo "" 62 | exit 1 63 | fi 64 | 65 | git tag -a "v${_sem_ver}" -m "New version ${_sem_ver}" && git push up "v${_sem_ver}" || { 66 | echo "Tagging failed. To force, type:" 67 | echo "" 68 | echo " git tag -a \"v${_sem_ver}\" -m \"New version ${_sem_ver}\" -f" 69 | echo " git push up "v${_sem_ver}" -f" 70 | echo "" 71 | exit 1 72 | } 73 | exit 0 74 | ;; 75 | new-version-branch) 76 | git checkout -b "PR-new-version-$(git rev-parse --short @)" 77 | exit 0 78 | ;; 79 | esac 80 | fi 81 | 82 | echo "$_sem_ver-$_commits-$_hash" 83 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 6.3.0 2 | --------------------------------------------------------------------------------