Effects
This page covers building effects, chaining them, writing them in direct style with Fx.gen, working with time, and running them. An effect (Effect<A, E>) is an immutable description of a program that succeeds with a value of type A or fails with an error of type E. Nothing runs until a fiber interprets it, so the same effect can run many times. Read this page first; Errors, Concurrency, Resources, Schedule and Schema build on it.
Examples print values with a --> result comment showing what they print.
How fibers run
A fiber runs one effect. In FiveM, fibers are plain Lua coroutines on a runner inside your resource, not Citizen threads:
- A fiber that waits (
Fx.sleep, aDeferred, aQueue, an RPC) is parked and costs nothing per tick. Waking it is acoroutine.resume. - All timers in a resource share one timer heap. A pump thread (rarely two) sleeps until the next timer is due.
Fx.sleepdoes not create a Citizen thread. Fx.runForkandeff:fork()do not start the fiber immediately. The fiber starts on the next pass of the runner (the next tick in FiveM).Fx.sleep(0)andFx.yieldNowgive the frame back; the fiber continues on the next tick.- A fiber that runs more than 2048 interpreter steps without waiting gives the frame back once, then continues on the next tick. Long synchronous chains do not freeze the game, but they also do not finish in one frame.
- A fiber woken by another fiber (a
Queueoffer, aDeferredcompleting) runs once the waking fiber parks, yields or finishes, never in the middle of it. - Code inside a fiber may still call
Wait,Citizen.Wait,Citizen.Awaitor a yielding native, inFx.genbodies and in plainFx.synccode. The fiber borrows a Citizen thread for that wait, the wait behaves exactly as on its own thread, and the fiber goes back to the runner the next time it parks.
Prefer Fx.sleep(ms) over Wait(ms) inside fibers. Fx.sleep stays on the runner and can be interrupted at once. A fiber blocked in Wait only notices an interruption after the Wait returns.
local stopped = Fx.gen(function()
Wait(1000) -- borrows a Citizen thread; not interruptible
Fx.yield(Fx.sleep(1000)) -- stays on the runner; interruptible
return 'done'
end)
Fx.runFork(stopped:timeout(500), {
onExit = function(exit) print(exit._tag, Fx.Cause.failureOption(exit.cause)._tag) end,
})
--> Failure TimeoutError
In the example above the timeout fires at 500 ms but the fiber is stuck in Wait until 1000 ms, so the TimeoutError arrives at 1000 ms, not 500 ms.
Building effects
Fx.succeed(value)
Fx.succeed(value: A) -> Effect<A, never>
- Parameters:
value, any Lua value includingnil. - Returns: an effect that succeeds with
value. - Fails with: never.
local starterCash = Fx.succeed(500)
print(Fx.runSync(starterCash)) --> 500
Fx.fail(err)
Fx.fail(err: E) -> Effect<never, E>
- Parameters:
err, the expected error. Usually a tagged error (see Errors), but any value works. - Returns: an effect that fails with
err. - Fails with:
err. - Notes: a tagged error instance has a shortcut:
NotEnoughMoney { need = 50 }:fail()is the same asFx.fail(NotEnoughMoney { need = 50 }).
local NotEnoughMoney = Fx.TaggedError('NotEnoughMoney')
local eff = Fx.fail(NotEnoughMoney { need = 50 })
local exit = Fx.runSyncExit(eff)
print(Fx.Cause.failureOption(exit.cause)._tag) --> NotEnoughMoney
Fx.die(defect)
Fx.die(defect: any) -> Effect<never, never>
- Parameters:
defect, any value describing a bug. - Returns: an effect that fails with a defect (an unexpected error). A Lua traceback of the call site is kept in the cause.
- Fails with: a defect, which
catchAll,catchTagand the other expected-error handlers do not catch. See Errors.
local exit = Fx.runSyncExit(Fx.die('vehicle table corrupted'))
print(Fx.Cause.defects(exit.cause)[1]) --> vehicle table corrupted
Fx.failCause(cause)
Fx.failCause(cause: Cause) -> Effect<never, any>
- Parameters:
cause, a Cause. - Returns: an effect that fails with exactly that cause.
- Fails with: whatever the cause holds.
- Notes: used to re-raise a cause you inspected in
catchAllCauseoronExit.
local eff = Fx.failCause(Fx.Cause.fail('busy'))
print(Fx.Cause.failureOption(Fx.runSyncExit(eff).cause)) --> busy
Fx.sync(fn)
Fx.sync(fn: fun(): A) -> Effect<A, never>
- Parameters:
fn, a function called with no arguments each time the effect runs. - Returns: an effect that succeeds with the return value of
fn. - Fails with: never by itself. If
fnraises a tagged error (error(NotFound {})), the effect fails with that error. Any other raised error becomes a defect with a traceback. - Notes:
fnruns lazily, every time. Iffnreturns an effect, that effect is the success value; it is not run. UseFx.suspendto build an effect lazily.
local balances = { [1] = 1200 }
local readBalance = Fx.sync(function() return balances[1] end)
balances[1] = 900
print(Fx.runSync(readBalance)) --> 900
Fx.try(fn, onError?)
Fx.try(fn: fun(): A, onError?: fun(err: any): TaggedError) -> Effect<A, UnknownError | TaggedError>
- Parameters:
fn, called with no arguments when the effect runs.onError, optional, maps a raised error to the failure. - Returns: an effect that succeeds with the return value of
fn. - Fails with:
UnknownError { message = tostring(err), cause = err }whenfnraises andonErroris not given (or returnsnil/false). A raised tagged error is kept as is. WithonError, the value it returns. - Notes: the value
onErrorreturns is raised again internally, so it must be a tagged error to become an expected failure. A plain string or table returned byonErrorbecomes a defect. Themessageof anUnknownErrorincludes Lua'sfile:line:prefix when the error was a string raised witherror('...').
local BadJson = Fx.TaggedError('BadJson')
local decode = function(text)
return Fx.try(function()
local v = load('return ' .. text)
if not v then error('not valid') end
return v()
end, function(err) return BadJson { message = tostring(err) } end)
end
local exit = Fx.runSyncExit(decode('{ oops'))
print(Fx.Cause.failureOption(exit.cause)._tag) --> BadJson
print(Fx.runSync(decode('42'))) --> 42
Fx.suspend(fn)
Fx.suspend(fn: fun(): Effect<A, E>) -> Effect<A, E>
- Parameters:
fn, called each time the effect runs. It must return an effect. - Returns: the effect
fnreturns. - Fails with: whatever the returned effect fails with. A raised tagged error is a failure; another raised error, or a return value that is not an effect, is a defect (
expected an Fx effect but got ...). - Notes: use it to read mutable state at run time, to branch, and for recursion.
local NotEnoughMoney = Fx.TaggedError('NotEnoughMoney')
local balances = { [1] = 100 }
local function withdraw(src, amount)
return Fx.suspend(function()
local have = balances[src] or 0
if have < amount then return NotEnoughMoney { have = have, need = amount }:fail() end
balances[src] = have - amount
return Fx.succeed(balances[src])
end)
end
print(Fx.runSync(withdraw(1, 30))) --> 70
print(Fx.runSyncExit(withdraw(1, 500))._tag) --> Failure
Fx.async(register)
Fx.async(register: fun(resume: fun(eff: Effect<A, E>)): (fun() | nil)) -> Effect<A, E>
- Parameters:
register, called when the effect runs. It receivesresume. Callresume(eff)once with the effect to continue with (Fx.succeed(v),Fx.fail(e)).registermay return a canceler function. - Returns: an effect that waits until
resumeis called, then runs the effect passed to it. - Fails with: whatever the resumed effect fails with. If
registerraises, the effect dies with that error. - Notes: the fiber parks while waiting.
resumemay be called synchronously insideregister. Only the first call counts. A value passed toresumethat is not an effect is treated asFx.succeed(value). If the fiber is interrupted while waiting, the canceler is called (errors in it are ignored) and the fiber is interrupted.
-- Wrap a callback API: a fake bank export that answers after a delay.
local function bankBalance(src, cb) Fx.runFork(Fx.sleep(50):zipRight(Fx.sync(function() cb(src * 100) end))) end
local balance = function(src)
return Fx.async(function(resume)
bankBalance(src, function(amount) resume(Fx.succeed(amount)) end)
end)
end
Fx.runFork(balance(3), { onExit = function(exit) print(exit.value) end }) --> 300
Fx.callback(register)
Fx.callback(register: fun(done: fun(ok: boolean, valueOrError: any)): (fun() | nil)) -> Effect<any, any>
- Parameters:
register, receivesdone. Calldone(true, value)to succeed ordone(false, err)to fail. It may return a canceler, as withFx.async. - Returns: an effect that waits for
done. - Fails with:
errfromdone(false, err). - Notes: a thin layer over
Fx.asyncfor node-style callbacks. It is an async effect even whendoneis called synchronously, so it cannot run underFx.runSync.
local function fetchGarage(src, cb)
if src == 1 then cb(true, { 'adder', 'zentorno' }) else cb(false, 'no garage') end
end
local garage = function(src)
return Fx.callback(function(done) fetchGarage(src, done) end)
end
Fx.runFork(garage(1), { onExit = function(exit) print(#exit.value) end }) --> 2
Fx.runFork(garage(2), { onExit = function(exit) print(Fx.Cause.failureOption(exit.cause)) end }) --> no garage
Fx.fromPromise(p)
Fx.fromPromise(p: promise) -> Effect<any, any>
- Parameters:
p, a Citizen promise (promise.new()), or any object withp:next(onResolve, onReject). - Returns: an effect that waits for the promise and succeeds with its value.
- Fails with: the rejection value.
- Notes: the promise is not cancelled if the fiber is interrupted.
-- In FiveM: local p = promise.new() ... p:resolve(value)
local p = { next = function(self, ok, fail) ok('Ana') end }
Fx.runFork(Fx.fromPromise(p), { onExit = function(exit) print(exit.value) end }) --> Ana
Fx.fromNullable(value, onNil?)
Fx.fromNullable(value: A | nil, onNil?: fun(): E) -> Effect<A, E | NoSuchElementError>
- Parameters:
value, any value.onNil, optional, returns the error to fail with. - Returns: an effect that succeeds with
valuewhen it is notnil. - Fails with:
onNil()when given, elseNoSuchElementError {}.
local PlayerNotFound = Fx.TaggedError('PlayerNotFound')
local function findName(src)
return Fx.fromNullable(GetPlayerName(src), function() return PlayerNotFound { src = src } end)
end
print(Fx.runSync(findName(1))) --> Ana
print(Fx.Cause.failureOption(Fx.runSyncExit(findName(99)).cause)._tag) --> PlayerNotFound
Fx.fromExit(exit)
Fx.fromExit(exit: Exit<A>) -> Effect<A, any>
- Parameters:
exit, an Exit. - Returns: an effect that succeeds with
exit.valueor fails withexit.cause.
print(Fx.runSync(Fx.fromExit(Fx.Exit.succeed('ok')))) --> ok
Fx.withFiber(f)
Fx.withFiber(f: fun(fiber: Fiber): Effect<A, E>) -> Effect<A, E>
- Parameters:
f, called with the running fiber; must return an effect. - Returns: the effect
freturns. - Notes: low level.
Fx.fiberIdcovers the common case.
local eff = Fx.withFiber(function(fiber) return Fx.succeed(type(fiber.id)) end)
print(Fx.runSync(eff)) --> number
Fx.unit and Fx.void
Fx.unit: Effect<nil, never>
Fx.void: Effect<nil, never>
- Returns: an effect that succeeds with
nil.Fx.voidis the same value asFx.unit. - Notes: finalizers,
onExitcallbacks and handlers that must return an effect often returnFx.unit.
print(Fx.runSync(Fx.unit)) --> nil
Fx.never
Fx.never: Effect<never, never>
- Returns: an effect that never finishes.
- Notes: it parks the fiber forever at no cost. The fiber stops only when interrupted (its parent ends, a timeout, a closed scope, resource stop). Useful to keep a scope open until the resource stops.
Fx.runFork(Fx.never:timeoutOption(100), { onExit = function(exit) print(exit.value) end }) --> nil
Fx.yieldNow
Fx.yieldNow: Effect<nil, never>
- Returns: an effect that gives the frame back to the game once and succeeds with
nilon the next tick. - Notes: under
Fx.runSyncit does nothing. Use it inside long loops of work to spread them over frames.
local function processAll(vehicles)
return Fx.forEach(vehicles, function(plate, i)
local step = Fx.sync(function() return plate:upper() end)
if i % 50 == 0 then return Fx.yieldNow:zipRight(step) end
return step
end)
end
Fx.runFork(processAll({ 'ab12', 'cd34' }), { onExit = function(exit) print(exit.value[2]) end }) --> CD34
Fx.fiberId
Fx.fiberId: Effect<integer, never>
- Returns: an effect that succeeds with the id of the fiber running it.
- Notes: fiber ids are unique per resource and increase. Effects run by
timeout,race,forEachwith concurrency andforkrun on other fibers and see other ids.
Fx.runFork(Fx.fiberId, { onExit = function(exit) print(math.type(exit.value)) end }) --> integer
Fx.isEffect(value)
Fx.isEffect(value: any) -> boolean
- Returns:
truewhenvalueis an effect (including service tags).
print(Fx.isEffect(Fx.unit), Fx.isEffect(function() end)) --> true false
Chaining
eff:map(f)
eff:map(f: fun(a: A): B) -> Effect<B, E>
- Parameters:
f, called with the success value; its return value is the new success value. - Returns: an effect with the mapped value.
- Fails with: the errors of
eff. A tagged error raised infbecomes a failure, any other raised error a defect.
local price = Fx.succeed(1999):map(function(cents) return cents / 100 end)
print(Fx.runSync(price)) --> 19.99
eff:flatMap(f)
eff:flatMap(f: fun(a: A): Effect<B, E2>) -> Effect<B, E | E2>
- Parameters:
f, called with the success value; must return an effect. - Returns: the effect returned by
f. - Fails with: the errors of
effor of the effectfreturns. A return value that is not an effect is a defect. - Notes: chains of any length do not grow the Lua stack.
local function getCash(src) return Fx.succeed(250) end
local function addCash(src, n) return Fx.succeed(250 + n) end
local eff = getCash(1):flatMap(function(cash) return addCash(1, cash) end)
print(Fx.runSync(eff)) --> 500
eff:andThen(next)
eff:andThen(next: Effect<B, E2> | fun(a: A): (Effect<B, E2> | B)) -> Effect<B, E | E2>
- Parameters:
next, an effect to run aftereff, or a function of the value that returns an effect or a plain value. - Returns: the next effect, or
Fx.succeed(value)when the function returns a plain value. - Notes: a mix of
map,flatMapandzipRight.
local eff = Fx.succeed(2):andThen(function(n) return n * 10 end):andThen(Fx.succeed('saved'))
print(Fx.runSync(eff)) --> saved
eff:tap(f)
eff:tap(f: fun(a: A): any) -> Effect<A, E>
- Parameters:
f, called with the success value. It may return an effect, which runs; any other return value is ignored. - Returns: an effect that keeps the original value.
- Fails with: the errors of
eff, or of the effectfreturns.
local eff = Fx.succeed(42):tap(function(n) print('spawned vehicle', n) end)
print(Fx.runSync(eff))
--> spawned vehicle 42
--> 42
eff:as(value)
eff:as(value: B) -> Effect<B, E>
- Returns: an effect that runs
effand succeeds withvalueinstead.
print(Fx.runSync(Fx.sync(function() end):as('paid'))) --> paid
eff:asVoid()
eff:asVoid() -> Effect<nil, E>
- Returns: an effect that runs
effand succeeds withnil.
print(Fx.runSync(Fx.succeed(5):asVoid())) --> nil
eff:zip(that)
eff:zip(that: Effect<B, E2>) -> Effect<{ [1]: A, [2]: B }, E | E2>
- Returns: an effect that runs
eff, thenthat, and succeeds with{ a, b }. - Notes: sequential. For concurrent runs use Fx.all.
local pair = Fx.runSync(Fx.succeed('Ana'):zip(Fx.succeed(1500)))
print(pair[1], pair[2]) --> Ana 1500
eff:zipLeft(that)
eff:zipLeft(that: Effect<B, E2>) -> Effect<A, E | E2>
- Returns: an effect that runs
eff, thenthat, and keeps the value ofeff.
local eff = Fx.succeed('veh_42'):zipLeft(Fx.sync(function() print('logged') end))
print(Fx.runSync(eff))
--> logged
--> veh_42
eff:zipRight(that)
eff:zipRight(that: Effect<B, E2>) -> Effect<B, E | E2>
- Returns: an effect that runs
eff, thenthat, and keeps the value ofthat.
print(Fx.runSync(Fx.sync(function() end):zipRight(Fx.succeed('next')))) --> next
eff:filterOrFail(pred, onFalse)
eff:filterOrFail(pred: fun(a: A): boolean, onFalse: fun(a: A): E2) -> Effect<A, E | E2>
- Parameters:
pred, checks the value.onFalse, builds the error whenpredis false. - Returns: an effect with the same value when
predholds. - Fails with:
onFalse(a)whenpred(a)is false.
local TooYoung = Fx.TaggedError('TooYoung')
local age = Fx.succeed(16):filterOrFail(function(a) return a >= 18 end, function(a) return TooYoung { age = a } end)
print(Fx.Cause.failureOption(Fx.runSyncExit(age).cause)._tag) --> TooYoung
eff:forever()
eff:forever() -> Effect<never, E>
- Returns: an effect that runs
effagain after every success, forever. - Fails with: the first failure of
eff, which stops the loop. - Notes: add a wait (
:delay(ms)) or the loop only gives the frame back every 2048 steps. For a counted or timed loop use repeatWith.
local ticks = 0
local loop = Fx.sync(function() ticks = ticks + 1 end):delay(1000):forever()
Fx.runFork(loop:timeoutOption(3500), { onExit = function() print(ticks) end }) --> 3
Error handling (catchAll, catchTag, mapError, result, match and the rest) is on the Errors page. Finalizers (ensuring, onExit, onError, onInterrupt) are on the Resources page.
Direct style with Fx.gen
Fx.gen(body, ...)
Fx.gen(body: fun(...): A, ...: any) -> Effect<A, any>
- Parameters:
body, a function written in direct style. Extra arguments are passed tobody. - Returns: an effect that runs
bodyin a new coroutine each time it runs, and succeeds with whatbodyreturns. Only the first return value is kept; return a table for several. - Fails with: the cause of the first
Fx.yieldwhose effect fails (the rest ofbodydoes not run). A tagged error raised witherror(MyError {})is a failure; any other raised error (including Lua runtime errors) is a defect with a traceback. - Notes:
Wait,Citizen.WaitandCitizen.Awaitwork insidebody(see How fibers run). UnderFx.runSyncthey are a defect.- When the fiber is interrupted while
bodyis suspended, the coroutine is closed: code after the pendingFx.yielddoes not run, and to-be-closed variables (<close>) are closed. pcallinsidebodydoes not catch effect failures. When a yielded effect fails,bodyis never resumed, so apcallaroundFx.yieldnever returns. Handle errors on the effect instead:Fx.yield(eff:result()).- Calling
Fx.yieldfrom a nested coroutine is a Lua error. Calling it from a plain function called bybodyis fine.
local NotEnoughMoney = Fx.TaggedError('NotEnoughMoney')
local balances = { [1] = 800 }
local getBalance = function(src) return Fx.sync(function() return balances[src] or 0 end) end
local setBalance = function(src, v) return Fx.sync(function() balances[src] = v end) end
local function buyVehicle(src, price)
return Fx.gen(function()
local have = Fx.yield(getBalance(src))
if have < price then error(NotEnoughMoney { have = have, need = price }) end
Fx.yield(setBalance(src, have - price))
return have - price
end)
end
print(Fx.runSync(buyVehicle(1, 300))) --> 500
local r = Fx.runSync(buyVehicle(1, 9000):result())
print(r.ok, r.error._tag, r.error.have) --> false NotEnoughMoney 500
Fx.yield(eff)
Fx.yield(eff: Effect<A, E>) -> A
- Parameters:
eff, the effect to run. - Returns: the success value of
eff, inside anFx.genbody. - Fails with: on failure the
Fx.genstops at this point and fails with the same cause. - Notes: outside
Fx.genit raisesFx.yield called outside Fx.gen .... Yielding something that is not an effect makes the gen die withFx.yield expects an effect, got <type>. Yield a service tag to get the service.
local eff = Fx.gen(function()
local a = Fx.yield(Fx.succeed(2))
local b = Fx.yield(Fx.succeed(3):delay(100))
return a * b
end)
Fx.runFork(eff, { onExit = function(exit) print(exit.value) end }) --> 6
eff:await()
eff:await() -> A
- Returns: the same as
Fx.yield(eff). Method form, only insideFx.gen.
local eff = Fx.gen(function()
local name = Fx.succeed('Ana'):await()
return 'Hello ' .. name
end)
print(Fx.runSync(eff)) --> Hello Ana
Time
Delays are in milliseconds and use the game timer (GetGameTimer).
Fx.sleep(ms)
Fx.sleep(ms: integer) -> Effect<nil, never>
- Parameters:
ms, delay in milliseconds.0, negative values and NaN mean "next tick". - Returns: an effect that succeeds with
nilafterms. - Notes: the fiber parks on the shared timer heap. Interruption cancels the timer at once.
local respawn = Fx.sleep(5000):zipRight(Fx.sync(function() print('respawned') end))
Fx.runFork(respawn) --> respawned
eff:delay(ms)
eff:delay(ms: integer) -> Effect<A, E>
- Returns: an effect that waits
ms, then runseff.
Fx.runFork(Fx.succeed('engine on'):delay(1500), { onExit = function(exit) print(exit.value) end }) --> engine on
eff:timeout(ms)
eff:timeout(ms: integer) -> Effect<A, E | TimeoutError>
- Parameters:
ms, the time limit. - Returns: an effect with the value of
effwhen it finishes in time. - Fails with:
TimeoutError { timeoutMs = ms }when the time runs out; the errors ofeffwhen it fails first. - Notes:
effruns on a child fiber. When the time runs out,effis interrupted and its finalizers run before the timeout error is raised. Ifeffis in an uninterruptible region, the timeout waits for that region to end. Whichever finishes first decides, so an early failure ofeffwins over the timer.
local loadModel = Fx.sleep(8000):as('adder')
Fx.runFork(loadModel:timeout(5000), {
onExit = function(exit)
local e = Fx.Cause.failureOption(exit.cause)
print(e._tag, e.timeoutMs)
end,
})
--> TimeoutError 5000
eff:timeoutFail(ms, onTimeout)
eff:timeoutFail(ms: integer, onTimeout: fun(): E2) -> Effect<A, E | E2>
- Parameters:
ms, the time limit.onTimeout, builds the error. - Fails with:
onTimeout()when the time runs out. - Notes: same interruption behaviour as
timeout.
local BankDown = Fx.TaggedError('BankDown')
local call = Fx.never:timeoutFail(3000, function() return BankDown { message = 'bank did not answer' } end)
Fx.runFork(call, { onExit = function(exit) print(tostring(Fx.Cause.failureOption(exit.cause))) end })
--> BankDown: bank did not answer
eff:timeoutOption(ms)
eff:timeoutOption(ms: integer) -> Effect<A | nil, E>
- Returns: an effect that succeeds with the value of
eff, or withnilwhen the time runs out. - Notes: a
TimeoutErrorraised byeffitself also becomesnil, because it is caught by tag.
local waitForPlayer = Fx.sleep(10000):as(3)
Fx.runFork(waitForPlayer:timeoutOption(2000), { onExit = function(exit) print(exit.value) end }) --> nil
eff:retry(policy)
eff:retry(policy: integer | Schedule | RetryPolicy) -> Effect<A, E>
- Parameters:
policy, a number of retries, a Schedule, or a table{ times?, schedule?, while?, until? }. - Returns: an effect that runs
effagain after each expected failure, as the policy allows. - Fails with: the last error when the policy stops. Defects and interruptions are not retried.
- Notes: full details on the Schedule page.
local attempts = 0
local flakyDb = Fx.suspend(function()
attempts = attempts + 1
if attempts < 3 then return Fx.fail('deadlock') end
return Fx.succeed('row saved')
end)
Fx.runFork(flakyDb:retry(Fx.Schedule.exponential(100):take(5)), {
onExit = function(exit) print(exit.value, attempts) end,
})
--> row saved 3
eff:repeatWith(schedule)
eff:repeatWith(schedule: Schedule | integer) -> Effect<A, E>
- Returns: an effect that runs
eff, then runs it again after each success as the schedule allows, and succeeds with the last value. - Fails with: the first failure, which stops the repeat.
- Notes:
eff['repeat'](eff, schedule)is the same function (repeatis a Lua keyword).eff:repeatN(n)runseffn + 1times. Details on the Schedule page.
local saves = 0
local autosave = Fx.sync(function() saves = saves + 1 end):repeatWith(Fx.Schedule.spaced(60000):take(2))
Fx.runFork(autosave, { onExit = function() print(saves) end }) --> 3
Fx.poll(check, timeout, onTimeout)
Fx.poll(check: fun(): boolean, timeout: integer, onTimeout: fun(): E) -> Effect<nil, E>
- Parameters:
check, called once per tick until it returns true.timeout, the time limit in ms.onTimeout, builds the error. - Returns: an effect that succeeds with
niloncecheck()is true. - Fails with:
onTimeout()aftertimeoutms. - Notes:
Fx.pollis defined by the assets module, so it isniluntilFx.assetshas been accessed once in the resource. TouchFx.assetsfirst (local _ = Fx.assets). For loading game assets use Fx.assets directly.
local _ = Fx.assets -- loads the module that defines Fx.poll
local ready = false
Fx.runFork(Fx.sync(function() ready = true end):delay(300))
local NotReady = Fx.TaggedError('NotReady')
Fx.runFork(Fx.poll(function() return ready end, 1000, function() return NotReady {} end), {
onExit = function(exit) print(exit._tag) end,
})
--> Success
Running effects
Every run* function accepts an optional opts.context table (tag to service) that becomes the fiber's initial services. Most code uses Runtime or eff:provide(layer) instead.
Fx.runFork(eff, opts?)
Fx.runFork(eff: Effect<A, E>, opts?: { onExit?: fun(exit: Exit<A>), context?: table }) -> Fiber
- Parameters:
eff, the effect.opts.onExit, called with the Exit when the fiber ends.opts.context, initial services. - Returns: the new root fiber, right away. It starts on the next tick.
- Fails with: nothing is raised. Without
onExit, a failure that is not only an interruption is logged as an error (Unhandled failure in fiber #<id>, with the pretty cause) and shows in/fxdev. WithonExit, nothing is logged; handling is up to you. - Notes: the fiber is interrupted when the resource stops. This is the usual way to start effects from event handlers and resource start.
local fiber = Fx.runFork(Fx.succeed('ready'):delay(100), {
onExit = function(exit) print(exit._tag, exit.value) end,
})
print(fiber.exit) -- not started yet
--> nil
--> Success ready
Fx.runCallback(eff, cb, opts?)
Fx.runCallback(eff: Effect<A, E>, cb: fun(exit: Exit<A>), opts?: { context?: table }) -> Fiber
- Returns: the fiber. Same as
Fx.runFork(eff, { onExit = cb, context = opts.context }).
Fx.runCallback(Fx.fail('no vehicle'), function(exit)
print(exit._tag, Fx.Cause.failureOption(exit.cause))
end)
--> Failure no vehicle
Fx.runAwait(eff, opts?)
Fx.runAwait(eff: Effect<A, E>, opts?: { context?: table }) -> A
- Parameters:
eff, the effect. - Returns: the success value. The calling Citizen thread is suspended until the effect finishes.
- Fails with: raises with
error(value, 0), sopcallworks: the error itself for an expected failure, the defect for a defect (a string defect gets its traceback appended), or a table{ _tag = 'Interrupted', _fxCause = cause }for an interruption. - Notes: call it from a Citizen thread (a
CreateThreadbody or an event handler), whereCitizen.Awaitwould also work. The effect runs on its own root fiber, interrupted on resource stop. This is the entry point for existing code that does not use effects yet.
CreateThread(function()
local price = Fx.runAwait(Fx.succeed(25000):delay(200))
print('price', price)
local ok, err = pcall(Fx.runAwait, Fx.fail('garage full'))
print(ok, err)
end)
--> price 25000
--> false garage full
Fx.runSync(eff, opts?)
Fx.runSync(eff: Effect<A, E>, opts?: { context?: table }) -> A
- Returns: the success value, computed right now on the calling thread.
- Fails with: raises like
Fx.runAwait. An effect that needs to wait (Fx.sleep,Fx.async,Deferred:await, aWaitinsideFx.gen) dies withFx.runSync hit an async effect. Use Fx.runFork or Fx.runAwait instead.(or theCitizen.Wait/Awaitvariant), which is raised. - Notes: for pure computations, schema decoding and tests. Any
Fx.asyncstep is rejected, even one whose callback answers synchronously, soFx.callback,Fx.fromPromise,Deferred:await,Queue:take,Semaphorepermits,timeout,raceand concurrentforEachcannot run here.Fx.yieldNowdoes nothing here. Forked fibers start later on the runner as usual.
print(Fx.runSync(Fx.succeed(2):map(function(x) return x * 2 end))) --> 4
local ok, err = pcall(Fx.runSync, Fx.sleep(10))
print(ok, err) --> false Fx.runSync hit an async effect. Use Fx.runFork or Fx.runAwait instead.
Fx.runSyncExit(eff, opts?)
Fx.runSyncExit(eff: Effect<A, E>, opts?: { context?: table }) -> Exit<A>
- Returns: the Exit of running
effsynchronously. Never raises for effect failures.
local exit = Fx.runSyncExit(Fx.fail('no license'))
print(exit._tag, Fx.Cause.failureOption(exit.cause)) --> Failure no license
Fx.setRuntime(rt)
Fx.setRuntime(rt: Runtime) -> nil
- Parameters:
rt, a Runtime. - Notes: makes
rtthe default runtime forFx.nethandlers,Fx.rpchandlers and theFx.compatox_lib bridge in this resource, so their handlers can yield the runtime's services. It does not affectFx.runFork,Fx.runAwaitorFx.runSync; usert:runFork(eff)for those. A per-handlerruntimeoption overrides it (see Net and RPC).
local Bank = Fx.Tag('Bank')
Fx.setRuntime(Fx.Runtime.make(Fx.Layer.succeed(Bank, { name = 'Maze Bank' })))
-- Fx.rpc.register(...) handlers can now Fx.yield(Bank).
Fx.setPlatform(p) and Fx.getPlatform()
Fx.setPlatform(p: table) -> nil
Fx.getPlatform() -> table
- Notes: swap the scheduler under the runtime. The repository's test runner (
lua5.4 tests/run.lua) uses a virtual-clock platform fromtests/platform.lua, so effects withFx.sleeprun instantly in tests. Game code never needs this.
-- In a test file run by tests/run.lua:
-- local Fx, p = T.fresh() -- fresh Fx on the virtual platform
-- local v = T.run(Fx, p, Fx.succeed(1):delay(5000))
-- assert(v == 1 and p.now() == 5000)
print(Fx.getPlatform().name) --> virtual