On this page
  1. tecs.Future
  2. Settlement
  3. Pumping and waiting
  4. Snapshots
  5. Module contents
    1. Types
    2. Functions
    3. Values
  6. Types
    1. Listener
    2. Source
  7. Functions
    1. all
    2. failed
    3. pending
    4. settled
    5. track
    6. tecs.Future:abandon
    7. tecs.Future:cancel
    8. tecs.Future:complete
    9. tecs.Future:fail
    10. tecs.Future:flatMap
    11. tecs.Future:map
    12. tecs.Future:onSettle
    13. tecs.Future:recover
    14. tecs.Future:wait
  8. Values
    1. error
    2. status
    3. value

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
end

tecs.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.sliceMs: number

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: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.

function tecs.Future.Source.advance(self, ms: number): integer
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.

function tecs.Future.Source.cancel(self, future: Future<any>)
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.

function tecs.Future.Source.poll(self): integer
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.

function tecs.Future.all<U>(inputs: {Future<U>}): Future<{U}>

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.

function tecs.Future.failed<U>(err: string): Future<U>

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.

function tecs.Future.pending<U>(source: Source): Future<U>

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.

function tecs.Future.settled<U>(value: U): Future<U>

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.

function tecs.Future.track<U>(
    world: World, entity: integer, key: string, future: Future<U>
)

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.

function tecs.Future.flatMap<U>(
    self, transform: function(T): Future<U>
): Future<U>

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.

function tecs.Future.map<U>(
    self, transform: function(T): U
): Future<U>

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.

function tecs.Future.onSettle(
    self, listener: function(Future<T>)
): Future<T>

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.

function tecs.Future.recover(
    self, recovery: function(string): T
): Future<T>

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.

function tecs.Future.wait(self, timeoutMs: number): Future<T>

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.error: string

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.status: string

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.

tecs.Future.value: T