# `tecs.assets`
Acquires assets without blocking the host, and owns what a caller holds.
A load returns the asset. There is no future, no promise and no callback in the
game-facing signature: a request that cannot answer at once parks its coroutine
through `nupp.suspension` and resumes with the answer, so the same call reads the
same way inside a frame system and inside a startup script.
```nupp
local loader = app.assets
local handle, reason = loader:request(tecs.assets.bytes, "levels/one.json")
if handle == nil then
return nil, reason
end
local text = handle:value() as string
handle:release()
```
[`Application`](tecs.application.Application) creates `app.assets` and shuts it
down after world shutdown. Standalone tools may use `newAssets` and must call
`shutdown` themselves. Settled handles remain valid until their owners release
them; stopping the loader cancels acquisitions, not those existing holds.
Inside a system the same two lines suspend the logical update rather than the
thread, and the host renders the frames that pass while the file is read. Where
no suspension handler is installed, the built-in driver runs the readiness pumps
on this thread and the call still returns the asset.
## What this module owns, and what it does not
This is orchestration: requesting, sharing an acquisition between callers that
want the same thing, counting holds, replacing a payload in place and reporting
a failure to the call that asked for it. Reading bytes, decoding pixels,
uploading a texture and freeing a native allocation belong to a
[`Kind`](tecs.assets.Kind), which supplies a
[`Job`](tecs.assets.Job) per acquisition and a `release` for the payload
it produced. A GPU service registers a kind whose job issues its own upload
command; nothing about textures is written here.
## Failure
An invalid argument raises at the call: an empty path, a released handle, a
loader that has shut down. A load that could not produce its asset answers
`nil, reason` from `request`, whether it failed before parking or hours later.
The readiness pump never raises a load failure of its own, because the call that
asked for the asset is the only place the reason means anything; the pump
resumes that call and lets it return the pair its signature promises.
## Holds and residency
A [`Handle`](tecs.assets.Handle) is a hold, not a cache entry. Two
requests for one path that overlap share one acquisition and receive holds on
one handle, because decoding the same file twice at once duplicates work without
producing another result. A request made after the first settles acquires again:
nothing here retains a settled asset behind the caller's back, and the payload
lives exactly as long as the holds on it.
`release` gives up one hold, and the last one frees the payload through the
kind. Releasing a handle already down to nothing does nothing, so a shutdown
path need not know whether something else got there first.
## Replacement
`reload` acquires the same path again and swaps the result into a handle every
holder already has, then frees what it displaced. The handle's identity does not
change and its `revision` advances, which is what a consumer holding device
state keyed on the handle watches to know its copy is stale. A failed reload
leaves the resident payload exactly as it was and answers `false, reason`.
## Constructors
### `newAssets` _constructor_
```nupp
function newAssets(): Assets
```
Creates a loader with nothing in flight and its readiness pump registered.
The pump reports itself inactive while nothing is in flight, so a program
with no suspension handler never sleeps on a loader that has no work.
#### Returns
| Type | Description |
| --- | --- |
| `Assets` | the running loader |
## Types
### `Assets` _record_
```nupp
record Assets
request: function(self: Assets, kind: Kind, path: string): (Handle?, string?)
reload: function(self: Assets, kind: Kind, handle: Handle): (boolean, string?)
drain: function(exclusive self: Assets): (boolean, string?)
pending: function(borrows self: Assets): integer
resident: function(borrows self: Assets): integer
running: function(borrows self: Assets): boolean
shutdown: function(exclusive self: Assets): nil
end
```
A loader: the acquisitions in flight, the holds they produced, and the
readiness pump that advances them.
One loader per application. Two loaders share nothing, which is what makes a
test able to shut one down without disturbing anything else in the process.
#### Methods
##### `request`
```nupp
request: function(self: Assets, kind: Kind, path: string): (Handle?, string?)
```
Requests one asset and returns the caller's hold on it.
The call suspends only while the acquisition is pending. Two overlapping
requests for one kind and path share the acquisition and receive holds on one
handle; a request made after the first settles acquires again.
The receiver is plain rather than `exclusive`, because the entry this
registers names the loader it belongs to and outlives the call.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `Assets` | |
| `kind` | `Kind` | |
| `path` | `string` | |
###### Returns
| Type | Description |
| --- | --- |
| `Handle?` | |
| `string?` | |
###### Raises
- when the path is empty or the loader has shut down
##### `reload`
```nupp
reload: function(self: Assets, kind: Kind, handle: Handle): (boolean, string?)
```
Acquires an asset again and replaces what a handle already holds.
Every holder sees the new payload through the handle it already has, and the
handle's `revision` advances so a consumer keyed on it can tell its own copy
is stale. The displaced payload is freed through the kind once the swap has
happened. A failed acquisition changes nothing and reports why.
The receiver is plain rather than `exclusive`, because the replacement entry
names the loader it belongs to and outlives the call.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `Assets` | |
| `kind` | `Kind` | |
| `handle` | `Handle` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
| `string?` | |
###### Raises
- when the loader has shut down, the handle is released, or the handle belongs to another loader or another kind
##### `drain`
```nupp
drain: function(exclusive self: Assets): (boolean, string?)
```
Suspends until nothing is in flight.
This is the startup and shutdown barrier, and it waits on every acquisition
this loader owns rather than only the caller's. It returns at once when
nothing is pending.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Assets` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
| `string?` | |
###### Raises
- when the loader has already shut down
##### `pending`
```nupp
pending: function(borrows self: Assets): integer
```
Returns the number of acquisitions still in flight.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Assets` | |
###### Returns
| Type | Description |
| --- | --- |
| `integer` | |
##### `resident`
```nupp
resident: function(borrows self: Assets): integer
```
Returns the number of handles still holding a payload.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Assets` | |
###### Returns
| Type | Description |
| --- | --- |
| `integer` | |
##### `running`
```nupp
running: function(borrows self: Assets): boolean
```
Reports whether this loader still accepts requests.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Assets` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
##### `shutdown`
```nupp
shutdown: function(exclusive self: Assets): nil
```
Stops the loader and reports the stop to every parked caller.
Every acquisition in flight is cancelled and every parked request resumes and
returns `nil, reason` from the call that asked for the asset, rather than
raising somewhere the reason means nothing. Handles that already settled keep
their payloads and still release correctly, so this frees nothing a caller
still holds.
Calling it twice does nothing the second time.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Assets` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
### `Handle` _record_
```nupp
record Handle
path: string
kind: string
value: function(borrows self: Handle): any?
resident: function(borrows self: Handle): boolean
revision: function(borrows self: Handle): integer
holds: function(borrows self: Handle): integer
retain: function(exclusive self: Handle): nil
release: function(exclusive self: Handle): nil
end
```
A caller's hold on one acquired asset.
Handles are shared: every caller whose request joined one acquisition gets
this same object, and the payload lives until the last of them releases it.
#### Methods
##### `value`
```nupp
value: function(borrows self: Handle): any?
```
Returns the payload this handle holds.
The kind that acquired it decides what this is, so the caller casts once:
`handle:value() as Image`. Nothing below checks that cast.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Handle` | |
###### Returns
| Type | Description |
| --- | --- |
| `any?` | |
##### `resident`
```nupp
resident: function(borrows self: Handle): boolean
```
Reports whether the payload is still in memory.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Handle` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
##### `revision`
```nupp
revision: function(borrows self: Handle): integer
```
Returns how many times this handle's payload has been replaced.
A consumer that keeps device state derived from the payload stores the
revision beside it and re-derives when the two differ.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Handle` | |
###### Returns
| Type | Description |
| --- | --- |
| `integer` | |
##### `holds`
```nupp
holds: function(borrows self: Handle): integer
```
Returns the number of callers holding this payload.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Handle` | |
###### Returns
| Type | Description |
| --- | --- |
| `integer` | |
##### `retain`
```nupp
retain: function(exclusive self: Handle): nil
```
Takes one more hold on this payload.
Call it when passing a handle somewhere with a lifetime of its own, so the
two owners release independently.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Handle` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
###### Raises
- when the payload has already been freed
##### `release`
```nupp
release: function(exclusive self: Handle): nil
```
Gives up this caller's hold, and frees the payload at the last one.
Call it once whatever needed the asset has taken ownership. Releasing a
handle already down to nothing does nothing, so a shutdown path need not know
whether something else got there first.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Handle` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### Fields
##### `path`
```nupp
path: string
```
Read-only. Contains the requested path unchanged. The loader does not
resolve it, so it is whatever the caller passed.
##### `kind`
```nupp
kind: string
```
Read-only. Contains the content kind name this asset was acquired under.
### `Job` _record_
```nupp
record Job
poll: function(): (Status, any?, string?)
cancel: function(): nil
end
```
One acquisition in progress, owned by the kind that opened it.
A job is driven by the loader's readiness pump, never by the caller. It runs
on whatever the kind uses underneath: a file read, a worker channel, a native
service reply. Nothing here assumes which.
#### Methods
##### `poll`
```nupp
poll: function(): (Status, any?, string?)
```
Advances the acquisition and reports where it stands.
Answers `"pending"` alone while the work continues, `"ready"` and the
payload once it is done, or `"failed"` and a non-empty reason. The loader
stops calling it after the first answer that is not `"pending"`, and the
payload it hands back becomes the loader's to free through the kind.
###### Returns
| Type | Description |
| --- | --- |
| `Status` | |
| `any?` | |
| `string?` | |
##### `cancel`
```nupp
cancel: function(): nil
```
Abandons the acquisition and frees anything it holds.
The loader calls it at most once, and only for a job that has not
answered `"ready"` or `"failed"`. It must not raise.
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
### `Kind` _record_
```nupp
record Kind
name: string
open: function(path: string): (Job?, string?)
release: (function(value: any): nil)?
end
```
How one class of asset is acquired and freed.
A payload is whatever the kind's job produced, so this layer types it as
`any` and the caller casts once at `value`. A phantom type parameter would
move that cast rather than check it, and the loader would still be handing
back what the job supplied.
#### Methods
##### `open`
```nupp
open: function(path: string): (Job?, string?)
```
Read-only. Begins one acquisition of `path`.
Answers the job that will produce the payload, or `nil, reason` where the
acquisition cannot start at all. A refusal here reaches the requesting
call without parking it.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `path` | `string` | |
###### Returns
| Type | Description |
| --- | --- |
| `Job?` | |
| `string?` | |
#### Fields
##### `name`
```nupp
name: string
```
Read-only. Contains the content kind name this asset is recorded under,
which is the string file watching and tooling filter on.
##### `release`
```nupp
release: (function(value: any): nil)?
```
Read-only. Frees one payload after its last hold goes. Nil where the
collector owns the payload outright, which is true of a plain Lua string.
### `Status` _type_
```nupp
type Status = "pending" | "ready" | "failed"
```
Where one acquisition stands.
## Functions
### `readJob` _function_
```nupp
function readJob(path: string): Job?, string?
```
Begins a complete file read that settles on the readiness pump.
The read itself runs in the pump's pass rather than in the requesting call,
so a request for a file parks and resumes exactly as one for a decoded asset
does. A kind that needs the bytes before it can do its own work composes this
job rather than reading the file inline.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `path` | `string` | the file to read |
#### Returns
| Type | Description |
| --- | --- |
| `Job?` | the job that answers the complete bytes |
| `string?` | a failure reason, when the read cannot be started |
## Values
### `bytes` _variable_
```nupp
const bytes: Kind
```
Reads a complete file into a binary-safe string without interpreting it.
A handle of this kind carries a `string`, so `handle:value() as string` is
the cast its caller writes. The collector owns the string and the kind frees
nothing. Releasing the handle still matters: it is what tells the loader the
asset is no longer resident.
### `DOCUMENT` _variable_
```nupp
const DOCUMENT
```
Read-only. Names the content kind of a file loaded without interpretation.
### `FONT` _variable_
```nupp
const FONT
```
Read-only. Names the content kind of a loaded font.
### `IMAGE` _variable_
```nupp
const IMAGE
```
Read-only. Names the content kind of a decoded image.
### `MODEL` _variable_
```nupp
const MODEL
```
Read-only. Names the content kind of a decoded model scene.
### `SHADER` _variable_
```nupp
const SHADER
```
Read-only. Names the content kind of a loaded shader.
### `SOUND` _variable_
```nupp
const SOUND
```
Read-only. Names the content kind of a loaded sound.