Core

The core is plain Lua 5.4 and loads into every resource that uses Rivo FX.

Fx.succeed / fail / die / sync / try / suspend / async build effects
:map :flatMap :andThen :tap :zip chain
:catchAll :catchTag :catchTags :mapError :orElse :result :option handle errors
Fx.gen(function() local x = Fx.yield(eff) end) direct style (eff:await() also works inside a gen)
:timeout(ms) :retry(policy) :repeatWith(schedule) :delay(ms) time
Fx.Schedule.exponential(100):take(5):jittered() retry and repeat schedules
:fork :forkDaemon :forkScoped, Fx.forEach(list, f, { concurrency = 4 }), Fx.all, :race concurrency
Fx.Tag, Fx.serviceOption(tag), Fx.Layer.succeed/effect/scoped/merge, :provide(layer), Fx.Runtime.make(layer) services
Fx.scoped, Fx.acquireRelease, Fx.addFinalizer, :ensuring, :onExit resources
Fx.Ref, Fx.Deferred, Fx.Semaphore, Fx.Queue, Fx.PubSub shared state
Fx.Stream values over time (see Streams)
Fx.S.* Schema
Fx.TaggedError('Name') typed errors
Fx.runFork / runAwait / runSync / runCallback run

Inside Fx.gen, Citizen.Wait and Citizen.Await still work. Raising a tagged error with error(NotFound {}) fails the effect with that error. Any other raised error is a defect.

How fibers run

A fiber is a coroutine on a runner inside your resource, not a Citizen thread, so a parked fiber costs nothing and waking one is a plain coroutine.resume. Timers share one thread per resource, which sleeps until the next one is due.

Code in a fiber that calls Wait or Citizen.Await (in Fx.gen or plain Fx.sync code) borrows a Citizen thread for that wait and behaves exactly as it would on its own thread. Prefer Fx.sleep over Wait inside fibers: it stays on the runner and can be interrupted.

A fiber woken by another fiber (a Queue offer, a Deferred) runs once the waker parks or yields, not in the middle of it.