Skip to content
Eugene Ghanizadeh edited this page Nov 23, 2022 · 11 revisions
function expr<R>(fn: ($: TrackFunc, _: TrackFunc) => R | typeof SKIP): Source<R>

type TrackFunc = {
  <T>(source: Source<T>): T
  n<T>(source: Source<T>): T | undefined
  on(...sources: Source<unknown>[]): void
}
                     a:  -----1------2-----3-|
                     b:  -2-------0-----3------6--|
expr($ => $(a) + $(b)):  -----3---1--2--5--6---9--|

Creates an expression of other sources. The expression is first computed when all sources have emitted at least once, and then is re-calculated whenever any of them emits a new value. For example, in the following code, the div will display the sum of the values of the inputs, when both inputs have a value:

HTML Code
<input type="number" id="a" />
<input type="number" id="b" />
<div></div>
import { pipe, event, expr, observe, tap, map } from 'streamlets'

const $a = document.querySelector('#a')
const $b = document.querySelector('#b')
const $div = document.querySelector('div')

const a = map(event($a, 'input'), () => parseInt($a.value))
const b = map(event($b, 'input'), () => parseInt($b.value))

pipe(
  expr($ => $(a) + $(b)),
  tap(x => $div.textContent = x),
  observe
)

Try in Sandbox

Expressions can also be conditionals (or any function really):

import { pipe, interval, event, expr, observe, tap, map } from 'streamlets'

const $a = document.querySelector('#a')
const $b = document.querySelector('#b')
const $div = document.querySelector('div')

const a = map(event($a, 'input'), () => parseInt($a.value))
const b = map(event($b, 'input'), () => parseInt($b.value))
const timer = interval(1000)

pipe(
  expr($ => {

    // alternate every second between displaying
    // the sum or the difference between the two
    // input values.
    //
    if ($(timer) % 2 === 0) {
      return $(a) + $(b)
    } else {
      return $(a) - $(b)
    }
  }),
  tap(x => $div.textContent = x),
  observe
)

Try in Sandbox

💡 A source will only be connected to when it is accessed in at least one run of the expression.


Return SKIP if you don't want to emit any values:

import { interval, pipe, expr, tap, observe, SKIP } from 'streamlets'

const timer = interval(1000)
pipe(
  expr($ => $(timer) % 2 === 0 ? $(timer) : SKIP),
  tap(console.log),
  observe
)

// > 0
// > 2
// > 4
// > ...

Try in Sandbox

⚠️ Don't create new sources inside the expression itself, as this would make it impossible for expr() to track values of each! Instead, create your sources outside of expr() and just combined them inside:

// ❌❌ THIS IS WRONG!! ❌❌
expr($ => $(interval(1000)) + $(event(btn, 'click')).clientX))
// ✅✅ This is CORRECT! ✅✅
const timer = interval(1000)
const click = event(btn, 'click')
expr($ => $(timer) + $(click).clientX)

Async Expressions

Expressions can be asynchronous functions:

const inp = event(input, 'input')

const response = expr(async $ => {
  const query = $(inp).value.toLowerCase()
  const resp = await fetch('https://my.api/?q=' + query)
  const json = await resp.json()

  return json
})

💡 Async expressions are cancelled mid-flight: if a source emits before the previous execution is finished, the previous execution will be cancelled: its result being ignored. Note that the execution itself is NOT stopped: i.e. if you make a request in your async expression, the request will definitely be sent even if the expression function is re-executed.


Passive Tracking

Track sources passively with the second argument passed to the expression function. If a passively tracked source emits, the expression won't be re-computed, but its emitted value will be used for the next run of the expression:

import { pipe, event, expr, observe, tap, map } from 'streamlets'

const $a = document.querySelector('#a')
const $b = document.querySelector('#b')
const $div = document.querySelector('div')

const a = map(event($a, 'input'), () => parseInt($a.value))
const b = map(event($b, 'input'), () => parseInt($b.value))

pipe(
  // now changing value of the second input won't
  // trigger an update in the displayed result.
  //
  expr(($, _) => $(a) + _(b)),
  tap(x => $div.textContent = x),
  observe
)

Try in Sandbox

💡 If no sources are actively tracked (considering conditionals), then the expression will end.


Higher-Level Streams

Flatten higher-level streams with chain applying of the $ function:

HTML Code
<button>(Re)Start Timer</button>
<div></div>
import { pipe, interval, event, expr, observe, tap, map } from 'streamlets'

const $btn = document.querySelector('button')
const $div = document.querySelector('div')

const click = event($btn, 'click')
const timers = expr($ => ($(click), interval(100)))
const timer = expr($ => $($(timers)) / 10)

pipe(
  timer,
  tap(x => $div.textContent = x),
  observe
)

Try in Sandbox

Explicit Dependencies

Use $.on() to explicitly specify all sources the expression should be dependent on:

HTML Code
<input type="number" />
<button>(Re)Start Timer</button>
<div></div>
import { pipe, interval, event, expr, observe, tap, map, prepend, of } from 'streamlets'

const $btn = document.querySelector('button')
const $input = document.querySelector('input')
const $div = document.querySelector('div')

const click = prepend(event($btn, 'click'), of({}))
const rate = map(event($input, 'input'), () => parseInt($input.value))

const timers = expr($ => {
  $.on(click)
  return interval($(rate) * 200)
})

pipe(
  expr($ => $($(timers)) / 5),
  tap(x => $div.textContent = x),
  observe
)

Try in Sandbox

💡 After explicitly outlining dependencies, all other sources will be passively tracked.


Nullish Start

Expressions don't emit until all of the sources they track (either actively or passively) emit at least once. In some cases, it is preferable to assume some default value for a source before its first emission. Use $.n() and/or _.n() instead of the usual tracking functions $() and _() to get undefined as the value for sources who have not emitted yet:

import { pipe, event, expr, observe, tap, map } from 'streamlets'

const $a = document.querySelector('#a')
const $b = document.querySelector('#b')
const $div = document.querySelector('div')

const a = map(event($a, 'input'), () => parseInt($a.value))
const b = map(event($b, 'input'), () => parseInt($b.value))

pipe(
  // now will start with an initial value of 0
  // before any user input.
  expr($ =>
    ($.n(a) ?? 0)        // default value for a (0)
    + ($.n(b) ?? 0)      // default value for b (also 0)
  ),
  tap(x => $div.textContent = x),
  observe
)

Try in Sandbox