Warning: Effeck is v0. The language will change in major, breaking ways. It is not suitable for production use.

SynchronizedRef

A SynchronizedRef serializes updates that need to run Effects. Use it when the next value depends on both the current value and work that can pause, such as refreshing a credential or loading a revision.

Serializing effectful updates

function recordRevisions() {
  const revisions = SynchronizedRef.make<ReadonlyArray<number>>([])
  forEach([1, 2, 3], (id) => {
    SynchronizedRef.updateEffect(revisions, (current) => {
      sleep("10 millis")
      return [...current, id]
    })
  }, { concurrency: 3 })
  return SynchronizedRef.get(revisions)
}

The callbacks acquire the reference's update lock one at a time, even though three fibers call them concurrently. Each callback sees the value committed by the previous update, so all three revisions remain in the final array. Return a replacement immutable value rather than changing the stored array in place.

Reading synchronized references

function inspect() {
  const state = SynchronizedRef.make({ version: 0, ready: false })
  const before = SynchronizedRef.get(state)
  SynchronizedRef.set(state, { version: 1, ready: true })
  const after = SynchronizedRef.get(state)
  return { before, after }
}

get reads the latest committed value; it does not wait for an in-progress update to compute a replacement. set, update, and modify are also available for synchronous changes. A separate read and later write do not become one atomic operation merely because the reference is synchronized.

Keeping dependent asynchronous updates atomic

error RefreshFailed

service Versions {
  loadAfter(version: number): number!RefreshFailed
}

function refreshTwice() {
  const version = SynchronizedRef.make(0)
  function refresh() {
    return SynchronizedRef.updateAndGetEffect(version, (current) =>
      Versions.loadAfter(current))
  }
  return all([refresh(), refresh()], { concurrency: 2 })
}
// [number, number]!RefreshFailed!Versions

The second update reads the first update's committed version before calling loadAfter. If a callback fails, it does not commit its replacement, and the lock is released. This protects the reference's update sequence; it does not roll back remote side effects, coordinate several references, or make recursive updates to the same reference safe.

Coming from Effect

Call updateEffect(ref, (current) => nextValue(current)) with an ordinary callback. Unlike the callback to Ref.update, this callback may pause and use services.