Skip to content

Commit

Permalink
Merge branch 'master' into update/sbt-git-2.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
cornerman authored Nov 12, 2023
2 parents a6e1898 + 357652d commit 583f2d1
Show file tree
Hide file tree
Showing 45 changed files with 4,501 additions and 1,759 deletions.
11 changes: 11 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Scala Steward: Reformat with scalafmt 3.5.9
94e3134c442fb9549a1c9a29c26d7bf739407f3a

# Scala Steward: Reformat with scalafmt 3.6.0
4e47cfe80dbadf536f9597d5b96f85e63cb14568

# Scala Steward: Reformat with scalafmt 3.6.1
38b0c3966278cb726d0da25ce8229683f2031c40

# Scala Steward: Reformat with scalafmt 3.7.0
e41ea66e8238a2e1e924f3845d923c5911a8f29a
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: 2

# supported package managers:
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem

updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
28 changes: 19 additions & 9 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
name: Test

on:
push:
branches: [master]
tags: [ v* ]
pull_request:
workflow_dispatch:

env:
SBT_OPTS: "-Xms1G -Xmx4G" # Default runner has 7G of RAM. https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources

jobs:
build:
strategy:
matrix:
scalaVersion: ["2.12.15", "2.13.8", "3.1.1"]
runs-on: ubuntu-20.04
scalaVersion: ["2.13.12", "3.3.1"]
runs-on: ubuntu-22.04

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: coursier/cache-action@v6
- uses: olafurpg/setup-scala@v12
- uses: olafurpg/setup-scala@v14

- name: Test
env:
NODE_OPTIONS: "--openssl-legacy-provider"
run: sbt "++${{matrix.scalaVersion}} -v test"

# - name: Debug over SSH (tmate)
Expand All @@ -30,19 +37,22 @@ jobs:

publish:
needs: [build]
if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v'))
# if this is not a pull request, run only on master or tags pushes.
# if this is a pull request, run only when 'publish' label is set
if: (github.event_name != 'pull_request' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v'))) ||
(github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'publish'))
strategy:
matrix:
os: [ubuntu-20.04]
scala: [2.13.8]
os: [ubuntu-22.04]
scala: [2.13.12]
java: [adopt@1.8]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- uses: olafurpg/setup-scala@v12
- uses: olafurpg/setup-scala@v14
with:
java-version: 8
- name: Cache SBT
Expand Down
25 changes: 15 additions & 10 deletions .mergify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,29 @@ queue_rules:
- name: default
conditions:
# Conditions to get out of the queue (= merged)
- check-success~=build \(2.12.\d+\)
- check-success~=build \(2.13.\d+\)
- check-success~=build \(3.1.\d+\)
- check-success~=build \(3.\d+.\d+\)

pull_request_rules:
- name: assign and label scala-steward's PRs

- name: Label dependency-update PRs
conditions:
- author=scala-steward
- or:
- author~=^dependabot(|-preview)\[bot\]$
- author=scala-steward
actions:
label:
add: [dependency-update]
- name: merge scala-steward's PRs

- name: Merge dependency-update PRs
conditions:
- base=master
- author=scala-steward
- check-success~=build \(2.12.\d+\)
- check-success~=build \(2.13.\d+\)
- check-success~=build \(3.1.\d+\)
- and:
- or:
- author~=^dependabot(|-preview)\[bot\]$
- author=scala-steward
- base=master
- check-success~=build \(2.13.\d+\)
- check-success~=build \(3.\d+.\d+\)
actions:
queue:
name: default
Expand Down
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
runner.dialect = scala213
version = "3.4.3"
version = "3.7.16"
maxColumn = 140
trailingCommas = always
align.preset = most
163 changes: 153 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,37 @@

# Colibri

A simple functional reactive library for scala-js. Colibri is an implementation of the `Observable`, `Observer` and `Subject` reactive concepts.
A simple functional reactive library for ScalaJS. Colibri is an implementation of the `Observable`, `Observer` and `Subject` reactive concepts.

If you're new to these concepts, here is a nice introduction from rx.js: <https://rxjs.dev/guide/overview>. Another good resource are these visualizations for common reactive operators: <https://rxmarbles.com/>.

This library includes:
- A (minimal) reactive library based on JavaScript native operations like `setTimeout`, `setInterval`, `setImmediate`, `queueMicrotask`
- A (minimal and performant) reactive library based on JavaScript native operations like `setTimeout`, `setInterval`, `setImmediate`, `queueMicrotask`
- Typeclasses to integrate with other streaming libraries

## Usage

Reactive core library with typeclasses:
```scala
libraryDependencies += "com.github.cornerman" %%% "colibri" % "0.4.1"
libraryDependencies += "com.github.cornerman" %%% "colibri" % "0.8.0"
```

```scala
import colibri._
```

Reactive variables with lazy, distinct, shared state variables (a bit like scala-rx, but lazy):
```scala
libraryDependencies += "com.github.cornerman" %%% "colibri-reactive" % "0.8.0"
```

```scala
import colibri.reactive._
```

For jsdom-based operations in the browser (`EventObservable`, `Storage`):
```scala
libraryDependencies += "com.github.cornerman" %%% "colibri-jsdom" % "0.4.1"
libraryDependencies += "com.github.cornerman" %%% "colibri-jsdom" % "0.8.0"
```

```scala
Expand All @@ -31,7 +41,7 @@ import colibri.jsdom._

For scala.rx support (only Scala 2.x):
```scala
libraryDependencies += "com.github.cornerman" %%% "colibri-rx" % "0.4.1"
libraryDependencies += "com.github.cornerman" %%% "colibri-rx" % "0.8.0"
```

```scala
Expand All @@ -40,7 +50,7 @@ import colibri.ext.rx._

For airstream support:
```scala
libraryDependencies += "com.github.cornerman" %%% "colibri-airstream" % "0.4.1"
libraryDependencies += "com.github.cornerman" %%% "colibri-airstream" % "0.8.0"
```

```scala
Expand All @@ -49,7 +59,7 @@ import colibri.ext.airstream._

For zio support:
```scala
libraryDependencies += "com.github.cornerman" %%% "colibri-zio" % "0.4.1"
libraryDependencies += "com.github.cornerman" %%% "colibri-zio" % "0.8.0"
```

```scala
Expand All @@ -58,7 +68,7 @@ import colibri.ext.zio._

For fs2 support (`Source` only):
```scala
libraryDependencies += "com.github.cornerman" %%% "colibri-fs2" % "0.4.1"
libraryDependencies += "com.github.cornerman" %%% "colibri-fs2" % "0.8.0"
```

```scala
Expand Down Expand Up @@ -90,7 +100,7 @@ val observable = Observable
.mapResource(x => myResource(x))
.switchMap(x => myObservable(x))
.debounceMillis(1000)

val observer = Observer.foreach[Int](println(_))

val subscription: Cancelable = observable.unsafeSubscribe(observer)
Expand All @@ -111,6 +121,12 @@ subject.unsafeOnNext(1)
val myEffect: IO[Unit] = subject.onNextF[IO](2)
```

### Memory management

Every subscription that is created inside of colibri methods is returned to the user. For example `unsafeSubscribe` or `subscribeF` returns a `Cancelable`. That means, the caller is responsible to cleanup the subscription by calling `Cancelable#unsafeCancel()` or `Cancelable#cancelF`.

If you are working with `Outwatch`, you can just use `Observable` without ever subscribing yourself. Then all memory management is handled for you automatically. No memory leaks.

## Typeclasses

We have prepared typeclasses for integrating with other streaming libaries. The most important ones are `Sink` and `Source`. `Source` is a typeclass for Observables, `Sink` is a typeclass for Observers:
Expand All @@ -126,14 +142,141 @@ In order to work with effects inside our Observable, we have defined the followi
- `RunEffect[F[_]]` can unsafely run an effect `F[_]` asynchronously, potentially starting synchronously until reaching an async boundary.
- `RunSyncEffect[F[_]]` can unsafely run an effect `F[_]` synchronously.

You can convert any `Source` into an `Observable` with `Observable.lift(source)`. The same for `Sink` and `Observer` with `Observer.lift(sink)`.

## Reactive variables

The module `colibri-reactive` exposes reactive variables. This is lazy, distinct shared state variables (internally using observables) that always have a value. These reactive variables are meant for managing state - opposed to managing events which is a perfect fit for lazy `Observable` in the core `colibri` library.

This module behaves similar to scala-rx - though variables are not hot and it is built on top of colibri Observables for seamless integration and powerful operators.

The whole thing is not entirely glitch-free, as invalid state can appear in operators like map or foreach. But you always have a consistent state in `now()` and it reduces the number of intermediate triggers or glitches. You can become completely glitch-free by converting back to observable and using `dropSyncGlitches` which will introduce an async boundary (micro-task).

A state variable is of type `Var[A] extends Rx[A] with RxWriter[A]`.

The laziness of variables means that the current value is only tracked if anyone subscribes to the `Rx[A]`. So an Rx does not compute anything on its own. You can still always call `now()` on it - if it is currently not subscribed, it will lazily calculate the current value.

Example:

```scala

import colibri.reactive._

val variable = Var(1)
val variable2 = Var("Test")

val rx = Rx {
s"${variable()} - ${variable2()}"
}

val cancelable = rx.unsafeForeach(println(_))

println(variable.now()) // 1
println(variable2.now()) // "Test"
println(rx.now()) // "1 - Test"

variable.set(2) // println("2 - Test")

println(variable.now()) // 2
println(variable2.now()) // "Test"
println(rx.now()) // "2 - Test"

variable2.set("Foo") // println("2 - Foo")

println(variable.now()) // 2
println(variable2.now()) // "Foo"
println(rx.now()) // "2 - Foo"

cancelable.unsafeCancel()

println(variable.now()) // 2
println(variable2.now()) // "Foo"
println(rx.now()) // "2 - Foo"

variable.set(3) // no println

// now calculates new value lazily
println(variable.now()) // 3
println(variable2.now()) // "Foo"
println(rx.now()) // "3 - Foo"
```

Apart from `Rx` which always has an initial value, there is `RxLater` (and `VarLater`) which will eventually have a value (both extend RxState which extends RxSource). It also meant for representing state just without an initial state. It is lazy, distinct and has shared execution just like `Rx`.

```
import colibri.reactive._
val variable = VarLater[Int]()
val stream1 = RxLater.empty
val stream2 = RxLater.future(Future.successful(1)).map(_ + 1)
val cancelable = variable.unsafeForeach(println(_))
val cancelable1 = stream1.unsafeForeach(println(_))
val cancelable2 = stream2.unsafeForeach(println(_))
println(variable.toRx.now()) // None
println(stream1.toRx.now()) // None
println(stream2.toRx.now()) // Some(2)
variable.set(13)
println(variable.toRx.now()) // Some(13)
```

There also exist `RxEvent` and `VarEvent`, which are event observables with shared execution. That is they behave like `Rx` and `Var` such that transformations are only applied once and not per subscription. But `RxEvent` and `VarEvent` are not distinct and have no current value. They should be used for event streams.

```
import colibri.reactive._
val variable = VarEvent[Int]()
val stream = RxEvent.empty
val mapped = RxEvent.merge(variable.tap(println(_)).map(_ + 1), stream)
val cancelable = mapped.unsafeForeach(println(_))
```

[Outwatch](https://github.com/outwatch/outwatch) works perfectly with Rx (or RxLater, RxEvent which all extend RxSource) - just like Observable.

```scala

import outwatch._
import outwatch.dsl._
import colibri.reactive._
import cats.effect.SyncIO

val component: VModifier = {
val variable = Var(1)
val mapped = rx.map(_ + 1)

val rx = Rx {
"Hallo: ${mapped()}"
}

div(rx)
}
```

### Memory management

The same principles as for Observables hold. Any cancelable that is returned from the API needs to be handled by the the caller. Best practice: use subscribe/foreach as seldomly as possible - only in selected spots or within a library.

If you are working with `Outwatch`, you can just use `Rx` without ever subscribing yourself. Then all memory management is handled for you automatically. No memory leaks.

## Information

Throughout the library, the type parameters for the `Sink` and `Source` typeclasses are named consistenly to avoid naming ambiguity when working with `F[_]` in the same context:
- `F[_] : RunEffect`
- `G[_] : Sink`
- `H[_] : Source`

Source Code: [Source.scala](colibri/src/main/scala/colibri/Source.scala), [Sink.scala](colibri/src/main/scala/colibri/Sink.scala)
Source Code: [Source.scala](colibri/src/main/scala/colibri/Source.scala), [Sink.scala](colibri/src/main/scala/colibri/Sink.scala), [RunEffect.scala](colibri/src/main/scala/colibri/effect/RunEffect.scala)

In general, we take a middle ground with pure functional programming. We focus on performance and ease of use. Internally, the code is mutable for performance reasons. Externally, we try to expose a typesafe, immutable, and mostly pure interface to the user. There are some impure methods for example for subscribing observables - thereby potentially executing side effects. These impure methods are named `unsafe*`. And there are normally pure alias methods returning an effect type for public use. The unsafe methods exist so they can be used internally - we try to keep extra allocations to a minimum there.

Types like `Observable` are conceptionally very similar to `IO` - they can just return more than zero or one value. They are also lazy, and operations like map/flatMap/filter/... do not actually do anything. It is only after you unsafely run or subscribe an Observable that it actually starts evaluating.

[Implementation for rx](rx/src/main/scala/colibri/ext/rx/package.scala)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.raquo.airstream.ownership.internalcolibri

import com.raquo.airstream.ownership._
import scala.scalajs.js
import com.raquo.ew.JsArray

object NoopOwner extends Owner {
override protected[this] val subscriptions: js.Array[Subscription] = null
override protected[this] val subscriptions: JsArray[Subscription] = null
override protected[this] def killSubscriptions(): Unit = ()
override protected[this] def onOwned(subscription: Subscription): Unit = ()
override private[ownership] def onKilledExternally(subscription: Subscription): Unit = ()
Expand Down
Loading

0 comments on commit 583f2d1

Please sign in to comment.