# tecs.Future A value that settles once, and the combinators over it. Assets, child processes, and HTTP requests return `Future`. Compose their results without blocking a frame: ```teal 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) 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`](/modules/Application/) frames do that automatically. `wait` pumps in blocking slices for startup, tools, and tests: ```teal local process, reason = tecs.io.Process.new({args = {"git", "status", "--porcelain"}}) if process == nil then error(reason) end local finished = 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`](/modules/Future/#tecs.Future.Listener) | type | Receives a settled future. | | [`Source`](/modules/Future/#tecs.Future.Source) | record | Defines how a future receives settlements and advances a blocking wait. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`all`](/modules/Future/#tecs.Future.all) | Static | Joins every input value into one future in input order. | | [`failed`](/modules/Future/#tecs.Future.failed) | Static | Creates a future that already carries a failure. | | [`pending`](/modules/Future/#tecs.Future.pending) | Static | Creates a future nothing has settled yet. | | [`settled`](/modules/Future/#tecs.Future.settled) | Static | Creates a future that already carries a value. | | [`track`](/modules/Future/#tecs.Future.track) | Static | Makes a future something a sequence can wait for. | | [`abandon`](/modules/Future/#tecs.Future.abandon) | Instance | Settles this future as canceled, if nothing has settled it yet. | | [`cancel`](/modules/Future/#tecs.Future.cancel) | Instance | Gives up this consumer's interest in the work. | | [`complete`](/modules/Future/#tecs.Future.complete) | Instance | Settles this future with a value, if nothing has settled it yet. | | [`fail`](/modules/Future/#tecs.Future.fail) | Instance | Settles this future as failed, if nothing has settled it yet. | | [`flatMap`](/modules/Future/#tecs.Future.flatMap) | Instance | Returns a future that adopts the future started by transform. | | [`map`](/modules/Future/#tecs.Future.map) | Instance | Returns a future carrying transform applied to this future's value. | | [`onSettle`](/modules/Future/#tecs.Future.onSettle) | Instance | Registers a dependent, and returns this future. | | [`recover`](/modules/Future/#tecs.Future.recover) | Instance | Returns a future that turns this future's failure into a value. | | [`wait`](/modules/Future/#tecs.Future.wait) | Instance | Blocks until this future settles, advancing its source. | ### Values | Value | Type | Description | | --- | --- | --- | | [`error`](/modules/Future/#tecs.Future.error) | `string` | Read-only. Explains a "failed" or "canceled" result. | | [`status`](/modules/Future/#tecs.Future.status) | `string` | Read-only. Reports "pending", "ready", "failed" or "canceled". | | [`value`](/modules/Future/#tecs.Future.value) | `T` | Read-only. Returns the result, waiting for it when necessary. | ## Types ### tecs.Future.Listener type Receives a settled future. ```teal type tecs.Future.Listener = function(Future) ``` ### 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. ```teal record tecs.Future.Source sliceMs: number defaultWaitMs: number advance: function(self, ms: number): integer cancel: function(self, future: Future) 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. ```teal 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. ```teal tecs.Future.Source.defaultWaitMs: number ``` #### 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. ```teal function tecs.Future.Source.advance(self, ms: number): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | [`Source`](/modules/Future/#tecs.Future.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. ```teal function tecs.Future.Source.cancel(self, future: Future) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | [`Source`](/modules/Future/#tecs.Future.Source) | The source that owns the work. | | `future` | [`Future`](/modules/Future/)`` | 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. ```teal function tecs.Future.Source.poll(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | [`Source`](/modules/Future/#tecs.Future.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. ```teal function tecs.Future.all(inputs: {Future}): Future<{U}> ``` #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `U` | | | #### Arguments | Name | Type | Description | | --- | --- | --- | | `inputs` | `{`[`Future`](/modules/Future/)`}` | 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`](/modules/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. ```teal function tecs.Future.failed(err: string): Future ``` #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `U` | | | #### Arguments | Name | Type | Description | | --- | --- | --- | | `err` | `string` | Nil becomes "failed". | #### Returns | Type | Description | | --- | --- | | [`Future`](/modules/Future/)`` | 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. ```teal function tecs.Future.pending(source: Source): Future ``` #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `U` | | | #### Arguments | Name | Type | Description | | --- | --- | --- | | `source` | [`Source`](/modules/Future/#tecs.Future.Source) | The optional source that advances blocking waits. | #### Returns | Type | Description | | --- | --- | | [`Future`](/modules/Future/)`` | 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. ```teal function tecs.Future.settled(value: U): Future ``` #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `U` | | | #### Arguments | Name | Type | Description | | --- | --- | --- | | `value` | `U` | The value to store without copying. | #### Returns | Type | Description | | --- | --- | | [`Future`](/modules/Future/)`` | 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. ```teal function tecs.Future.track( world: World, entity: integer, key: string, future: Future ) ``` #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `U` | | | #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.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`](/modules/Future/)`` | 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. ```teal function tecs.Future.abandon(self, err: string) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Future` | 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. ```teal function tecs.Future.cancel(self) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Future` | 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. ```teal function tecs.Future.complete(self, value: T) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Future` | 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. ```teal function tecs.Future.fail(self, err: string) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Future` | 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. ```teal function tecs.Future.flatMap( self, transform: function(T): Future ): Future ``` #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `U` | | | #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | [`Future`](/modules/Future/)`` | The future whose ready value starts the dependent work. | | `transform` | `function(T): `[`Future`](/modules/Future/)`` | 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`](/modules/Future/)`` | 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. ```teal function tecs.Future.map( self, transform: function(T): U ): Future ``` #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `U` | | | #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | [`Future`](/modules/Future/)`` | 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`](/modules/Future/)`` | 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. ```teal function tecs.Future.onSettle( self, listener: function(Future) ): Future ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Future` | The future on which to register the listener. | | `listener` | `function(`[`Future`](/modules/Future/)`)` | 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`](/modules/Future/)`` | 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. ```teal function tecs.Future.recover( self, recovery: function(string): T ): Future ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Future` | 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`](/modules/Future/)`` | 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. ```teal function tecs.Future.wait(self, timeoutMs: number): Future ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Future` | 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`](/modules/Future/)`` | 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. ```teal 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. ```teal 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. ```teal tecs.Future.value: T ```