On this page
tecs.Future
A value that settles once, and the combinators over it.
Assets, child processes, and HTTP requests return Future<T>. Compose their results without blocking a frame:
local process, reason = tecs.io.Process.new({args = {"git", "status", "--porcelain"}})
if process == nil then
error(reason)
end
process.finished
:map(function(exit: tecs.io.Process.Exit): boolean
return exit:succeeded()
end)
:recover(function(): boolean
return false
end)
:onSettle(function(succeeded: tecs.Future<boolean>)
print(succeeded.value)
process:close()
end)Settlement
status starts as "pending" and moves once to "ready", "failed", or "canceled". A process exit code or HTTP error status still produces a ready result because the operation returned an answer. Failure means the operation produced none.
Reading value is the blocking dereference. It advances a pending future for its source's default wait budget, returns a ready result, and raises on failure, cancellation, or a budget that expires first. Successful non-nil results are stored directly, so reads after settlement are ordinary field reads. Use status and error when inspecting an outcome without raising.
map transforms a ready value. flatMap starts dependent work and adopts its future. recover turns failure into a value. Canceled futures skip all three transforms except cancellation propagation.
Listeners run in registration order. With no delivery in progress, registering a listener after settlement runs it before onSettle returns. During delivery, a new listener joins the current drain instead of growing the Lua stack. It may run after that nested onSettle returns, but before the outermost drain returns.
Pumping and waiting
A future advances when its subsystem pumps the source. Application frames do that automatically. wait pumps in blocking slices for startup, tools, and tests:
local process, reason = tecs.io.Process.new({args = {"git", "status", "--porcelain"}})
if process == nil then
error(reason)
end
local finished <const> = process.finished:wait(2000)
if finished.status == "ready" then
print(finished.value.exitCode)
end
process:close()Do not call wait or read a pending value from a frame. A timeout leaves the future pending. A listener must not wait on another pending future driven by the same non-reentrant source.
Waiting until a subsystem has nothing outstanding is that subsystem's own drain rather than a join over futures a caller holds. Future.all reads its inputs once and holds each one, so it answers for a set fixed at the call and not for a set that work started during the wait joins.
A source must settle its futures on the same thread that pumps it.
Canceling releases one consumer's interest. Shared operations stop only after their last consumer releases the root future.
Snapshots
A snapshot never stores a future because it contains runtime listeners and source state. A saved sequence cursor retains only its provider, entity, and key. Reissue and track the work under the same key after load when the wait must continue.
So a future is not a component field. Keep it in a table beside the world, keyed by the entity waiting on it, and give that entity a transient marker instead. A snapshot writer that reaches a future walks its listener graph rather than refusing the component, so the save fails without naming either.
Module contents
Types
| Type | Kind | Description |
|---|---|---|
Listener |
type | Receives a settled future. |
Source |
record | Defines how a future receives settlements and advances a blocking wait. |
Functions
| Function | Kind | Description |
|---|---|---|
all |
Static | Joins every input value into one future in input order. |
failed |
Static | Creates a future that already carries a failure. |
pending |
Static | Creates a future nothing has settled yet. |
settled |
Static | Creates a future that already carries a value. |
track |
Static | Makes a future something a sequence can wait for. |
abandon |
Instance | Settles this future as canceled, if nothing has settled it yet. |
cancel |
Instance | Gives up this consumer's interest in the work. |
complete |
Instance | Settles this future with a value, if nothing has settled it yet. |
fail |
Instance | Settles this future as failed, if nothing has settled it yet. |
flatMap |
Instance | Returns a future that adopts the future started by transform. |
map |
Instance | Returns a future carrying transform applied to this future's value. |
onSettle |
Instance | Registers a dependent, and returns this future. |
recover |
Instance | Returns a future that turns this future's failure into a value. |
wait |
Instance | Blocks until this future settles, advancing its source. |
Values
| Value | Type | Description |
|---|---|---|
error |
string |
Read-only. Explains a "failed" or "canceled" result. |
status |
string |
Read-only. Reports "pending", "ready", "failed" or "canceled". |
value |
T |
Read-only. Returns the result, waiting for it when necessary. |
Types
tecs.Future.Listener type
Receives a settled future.
type tecs.Future.Listener = function(Future<any>)tecs.Future.Source record
Defines how a future receives settlements and advances a blocking wait.
Every source in this tree is something that can be told to block for up to N milliseconds and hand over whatever arrived, which is the whole of what a future needs from the work behind it. An asset decode and a child process are a worker channel; an HTTP transfer is a Reqwest task whose bounded event queue has the same shape.
record tecs.Future.Source
sliceMs: number
defaultWaitMs: number
advance: function(self, ms: number): integer
cancel: function(self, future: Future<any>)
poll: function(self): integer
endtecs.Future.Source.sliceMs field
Caller-writable. Sets the maximum milliseconds one wait slice blocks. The source may omit it to use 16.
tecs.Future.Source.defaultWaitMs field
Caller-writable. Sets the milliseconds wait spends when its caller supplies no timeout. The source may omit it to use 5000.
tecs.Future.Source.defaultWaitMs: numbertecs.Future.Source:advance Instance
Caller-writable. Blocks up to ms for one result and settles its future.
What Future:wait spends a slice in, so it must not block for longer than the requested duration.
Arguments
| Name | Type | Description |
|---|---|---|
self |
Source |
The source to advance. |
ms |
number |
The maximum number of milliseconds to block. |
Returns
| Type | Description |
|---|---|
integer |
Returns the number of futures settled while advancing. |
tecs.Future.Source:cancel Instance
Caller-writable. Stops the work behind a root future after its last watcher leaves.
Optional. A source with no hook leaves the work running and stops caring, which is what a future over something uncancelable can honestly do.
Arguments
| Name | Type | Description |
|---|---|---|
self |
Source |
The source that owns the work. |
future |
Future<any> |
The root future whose last watcher left. |
Returns
None.
tecs.Future.Source:poll Instance
Caller-writable. Takes every ready result and settles its future.
Called once per frame per source, and costs a non-blocking receive that answers nil on any frame where nothing finished.
Arguments
| Name | Type | Description |
|---|---|---|
self |
Source |
The source to poll. |
Returns
| Type | Description |
|---|---|
integer |
Returns the number of futures settled by this poll. |
Functions
tecs.Future.all Static
Joins every input value into one future in input order.
Settles when the last input does, whatever order they settle in. A failed or canceled input fails the join with that input's error; the rest are left running, because an input shared with something outside the join is not this join's to stop. A caller that wants every outcome rather than the first failure puts a recover on each input, which is the shape a value that may be missing already uses.
Fan-in does not nest: each listener decrements a counter and only the last input settles the join, so a join over five hundred loads is two deep rather than five hundred.
Joining takes a hold on every input, so an input the join is still waiting for is not abandoned while the join lives, and canceling the join releases every one of those holds. A join dropped without being canceled keeps them, which is why a barrier over work another subsystem started is a drain on that subsystem rather than a join here: the hold would outlive the wait and leave that subsystem's own cancel unable to reach zero.
An input that settles "ready" carrying nil fails the join rather than filling its slot, because nil is a legal value and an array cannot hold one: the slot would read as absent and every index past it would be past a hole. A caller with a value that may be missing recovers to something rather than to nil.
Type Parameters
| Name | Constraint | Description |
|---|---|---|
U |
Arguments
| Name | Type | Description |
|---|---|---|
inputs |
{Future<U>} |
Read once; adding to the list afterwards changes nothing. Empty gives an already-ready future over an empty array. Nil raises. |
Returns
| Type | Description |
|---|---|
Future<{U}> |
A future whose value is a fresh array in inputs order. The join settles "ready" only after every slot contains a value, so no hole can appear. |
tecs.Future.failed Static
Creates a future that already carries a failure.
Type Parameters
| Name | Constraint | Description |
|---|---|---|
U |
Arguments
| Name | Type | Description |
|---|---|---|
err |
string |
Nil becomes "failed". |
Returns
| Type | Description |
|---|---|
Future<U> |
A future at "failed" with no source, so wait on it returns at once and cancel does nothing. map on it propagates without running the transform, while recover on it is the one thing that still runs. |
tecs.Future.pending Static
Creates a future nothing has settled yet.
The producer half, for a source settling its own work and for a test that decides when something finishes. source is what a wait on it advances. When omitted, only code that holds the future can settle it, and a wait on it returns at once.
Type Parameters
| Name | Constraint | Description |
|---|---|---|
U |
Arguments
| Name | Type | Description |
|---|---|---|
source |
Source |
The optional source that advances blocking waits. |
Returns
| Type | Description |
|---|---|
Future<U> |
A future at "pending" with one watcher, so a single cancel on it settles it "canceled" and calls the source's hook. |
tecs.Future.settled Static
Creates a future that already carries a value.
Type Parameters
| Name | Constraint | Description |
|---|---|---|
U |
Arguments
| Name | Type | Description |
|---|---|---|
value |
U |
The value to store without copying. |
Returns
| Type | Description |
|---|---|
Future<U> |
A future at "ready" with no source, so wait on it returns at once and cancel does nothing. A listener added to it follows the synchronous and reentrant delivery contract documented on onSettle. |
tecs.Future.track Static
Makes a future something a sequence can wait for.
The sequencer has a registry for waiting on work outside it, and this module is its one engine registrant, under the name "tecs.future". So a program parks on a decode, a subprocess or a request with
sequence.await("tecs.future", sequence.bind("loader"), "level1")
against a future tracked here under that same entity and key.
Three properties of the machinery underneath, none of them this function's to change. An await needs a live bound entity, because the VM releases a cursor whose entity is dead rather than stranding it, so the key is a pair and the entity is the game's own loader. The key is a program constant, so a program awaits a future by the name used to track it under rather than by identity, which is what lets the wait survive a snapshot the future cannot. And only the fixed clock delivers awaits, so a program on the frame or presentation clock parked on a future is released by a fixed step; that is already true of every awaitable.
Tecs drops the entry when the future settles, so a program that reaches its await after the work finished carries on rather than hanging, and nothing accumulates. Tracking a second future under one entity and key replaces the first.
There is no setPaused: the asset worker cannot pause a decode in flight, and the contract says a provider that cannot pause its work does not offer it.
Type Parameters
| Name | Constraint | Description |
|---|---|---|
U |
Arguments
| Name | Type | Description |
|---|---|---|
world |
World |
The world that owns the bound entity. |
entity |
integer |
What the await binds to, and it must still be alive when the program parks; the VM releases a cursor whose entity is dead. |
key |
string |
A program constant, not something derived per call, since the await names it. Nil or empty raises. |
future |
Future<U> |
Nil raises. One that has already settled is deliberately not written down, so a later await on that key carries straight on rather than hanging. |
Returns
None.
tecs.Future:abandon Instance
Settles this future as canceled, if nothing has settled it yet.
The producer half of the third terminal state, and the counterpart of cancel rather than a spelling of it: cancel is a consumer saying it no longer wants the work and counts the others who might, while this states an outcome that has already happened. A child killed on request or at its deadline reaches its future through here, as does a transfer taken off the multi.
function tecs.Future.abandon(self, err: string)Arguments
| Name | Type | Description |
|---|---|---|
self |
Future<T> |
The future to settle as canceled. |
err |
string |
Nil becomes "canceled". No watcher counting happens here: this settles the future outright, whoever else was waiting on it. |
Returns
None.
tecs.Future:cancel Instance
Gives up this consumer's interest in the work.
Reference counted, because a shared root is real: two loads of one path that overlap get the same future, and one of them canceling must not break the other. So this decrements, and only the last one settles the future "canceled" and asks the source to stop the work. Canceling a derived future drops its listener and decrements its upstream, which may in turn reach zero.
Canceling a settled future is a no-op.
function tecs.Future.cancel(self)Arguments
| Name | Type | Description |
|---|---|---|
self |
Future<T> |
The future whose consumer interest to release. |
Returns
None.
tecs.Future:complete Instance
Settles this future with a value, if nothing has settled it yet.
The producer half. Whoever created a future through Future.pending owns this and its sibling fail; a future handed out by a subsystem is settled by that subsystem's source, and calling this on one is a caller settling work it does not own. Settling twice is a no-op, so the first answer wins.
function tecs.Future.complete(self, value: T)Arguments
| Name | Type | Description |
|---|---|---|
self |
Future<T> |
The future to settle. |
value |
T |
Stored as it is, not copied, and read back off value. Nil is a legal value and still settles "ready", which is how a future over nothing settles. |
Returns
None.
tecs.Future:fail Instance
Settles this future as failed, if nothing has settled it yet.
function tecs.Future.fail(self, err: string)Arguments
| Name | Type | Description |
|---|---|---|
self |
Future<T> |
The future to fail. |
err |
string |
A sentence for a log, not something to match on. Nil becomes "failed", so error is set on every failed future. |
Returns
None.
tecs.Future:flatMap Instance
Returns a future that adopts the future started by transform.
The one combinator that expresses a dependent second request. Two things follow from transform starting work rather than transforming work already done: the derived future's source becomes the inner future's, so a chain can begin on a worker and end somewhere else; and canceling before the outer settles prevents any call to transform, since there is nothing yet to cancel afterwards.
Type Parameters
| Name | Constraint | Description |
|---|---|---|
U |
Arguments
| Name | Type | Description |
|---|---|---|
self |
Future<T> |
The future whose ready value starts the dependent work. |
transform |
function(T):Future<U> |
Called at most once, on "ready" only. Returning nil rather than a future fails this link; nil for transform itself raises here and now. |
Returns
| Type | Description |
|---|---|
Future<U> |
A new future settling to whatever the inner one settles to, status and all, so canceling an inner leaves this future canceled and not failed. |
tecs.Future:map Instance
Returns a future carrying transform applied to this future's value.
transform runs only on "ready". A "failed" or "canceled" upstream propagates without calling it, and a raise inside it becomes this link's failure.
Type Parameters
| Name | Constraint | Description |
|---|---|---|
U |
Arguments
| Name | Type | Description |
|---|---|---|
self |
Future<T> |
The future whose ready value to transform. |
transform |
function(T): U |
Called at most once, with the upstream value. Nil raises here and now rather than at settlement. |
Returns
| Type | Description |
|---|---|
Future<U> |
A new future watching this one. Canceling it gives up this link's interest in the upstream and does not settle the upstream unless nothing else wants it. |
tecs.Future:onSettle Instance
Registers a dependent, and returns this future.
The primitive the others are built on. With no delivery in progress, a listener on a future that has already settled runs before this call returns. During delivery, a new listener joins the current drain instead of growing the Lua stack. It may run after this nested call returns, but before the outermost drain returns. A listener that raises writes to the log and does not stop the others.
Arguments
| Name | Type | Description |
|---|---|---|
self |
Future<T> |
The future on which to register the listener. |
listener |
function(Future<T>) |
Called once, with this future, whichever of the three terminal states it reached; read status rather than assuming a value. Nil raises. |
Returns
| Type | Description |
|---|---|
Future<T> |
This same future, not a derived one, so chaining onSettle registers several listeners on one thing rather than building a chain. |
tecs.Future:recover Instance
Returns a future that turns this future's failure into a value.
recovery runs only on "failed". A "canceled" upstream is not recovered, which is the whole reason the two are distinct states.
Arguments
| Name | Type | Description |
|---|---|---|
self |
Future<T> |
The future whose failure to recover. |
recovery |
function(string): T |
Given the upstream's error string. Raising inside it fails this link with that raise instead, so a recovery cannot fail silently. Nil raises here and now. |
Returns
| Type | Description |
|---|---|
Future<T> |
A new future watching this one, carrying the same type: a recovery produces a value where the upstream produced an error, so a ready upstream passes through untouched. |
tecs.Future:wait Instance
Blocks until this future settles, advancing its source.
For startup, tools and tests; a frame polls status instead. Never a busy loop: the time is spent inside the source's blocking receive. The budget is wall clock rather than a count of slices, so a source that answers faster than its slice size does not shorten the wait.
A future with no source cannot advance and returns at once.
Arguments
| Name | Type | Description |
|---|---|---|
self |
Future<T> |
The future to wait for. |
timeoutMs |
number |
How long to wait. Defaults to the source's defaultWaitMs, or 5000. Running out leaves it "pending". |
Returns
| Type | Description |
|---|---|
Future<T> |
This future. |
Values
tecs.Future.error variable
Read-only. Explains a "failed" or "canceled" result. The source writes it, and game code should not assign it.
tecs.Future.status variable
Read-only. Reports "pending", "ready", "failed" or "canceled". The source changes it once, and game code should not assign it.
tecs.Future.value variable
Read-only. Returns the result, waiting for it when necessary.
A pending read advances the source for its default wait budget. A "failed" or "canceled" result raises with error, as does a future that remains pending after that budget. A pending future with no source raises immediately because nothing can advance it. Waiting here has the same frame and source-reentrancy constraints as wait.
The source writes this field, and game code should not assign it.