# `tecs.watch`
Watches content for changes and hands settled ones to reload handlers.
```nupp
tecs.watch.on("shader", function(change: tecs.watch.Change): nil
print("reload " .. change.path)
end)
tecs.watch.add(tecs.files.assetPath("shaders/lit.wgsl"))
tecs.watch.install({intervalSeconds = 0.25})
```
A host advances the watcher by calling `poll` once per turn and `dispatch` once
per logical update. `poll` looks at the watched paths when the interval has
elapsed and queues what has settled; `dispatch` runs the handlers over that
queue. Keeping the two apart is what stops a handler from running inside a
half-completed phase: a reload replaces a resource other systems are reading,
so it belongs at a point the frame has declared, not wherever a poll happened to
land.
## Settling
An editor commonly saves by truncating and rewriting, or by writing a temporary
file and renaming it over the target. A watcher that dispatched the first change
it saw would hand a reload handler half a file. So a change is dispatched only
after the same size and modification time have been observed `settle` extra
times, and a path that is missing, empty, or a directory when a scan lands is
treated as a save in progress rather than as a change.
A path that changes and changes back inside the settle window dispatches
nothing, because the bytes on disk are the bytes that were already there.
## What is watched
Every watched path is registered explicitly through `add`. Content reads use
`nupp.io.files`, which does not maintain a process-wide set of paths. The caller
that loaded a file therefore decides whether to watch it. No root fence is
needed because every watched path is named directly.
## The queue
Settled changes wait in a bounded queue, and repeated changes to one path
coalesce while they wait. `capacity` bounds distinct pending paths; an overflow
is logged and left for a later scan rather than growing without limit. A handler
that raises reaches the log and does not stop the rest of the batch, because a
handler raising has said the file is not usable yet, which is the same answer as
waiting for the next save.
## Polling rather than platform notification
This polls `nupp.io.files.info` and does not use a platform change-notification
service. A notification says a write happened, not that the writer finished,
so the settle policy would still have to stat the file. Polling also keeps
platform notification threads outside the Lua virtual machine and fits the
bounded set of paths a game explicitly registered.
## Types
### `Change` _record_
```nupp
record Change
path: string
kind: string
end
```
One settled content change.
#### Fields
##### `path`
```nupp
path: string
```
Read-only. Names the changed path, exactly as it was registered.
##### `kind`
```nupp
kind: string
```
Read-only. Names the content kind the path was registered under. The
built-in loaders use `"image"`, `"sound"`, `"font"`, `"shader"`,
`"model"` and `"document"`, which are the `tecs.assets` kind names.
### `ChangeHandler` _type_
```nupp
type ChangeHandler = function(change: Change): nil
```
Receives one settled change.
### `Config` _type_
```nupp
type Config = {
intervalSeconds: number?,
settle: integer?,
capacity: integer?,
clock: (function(): number)?,
source: Source?
}
```
Settings copied by `install`.
`intervalSeconds` sets the delay between polls and defaults to 0.5.
`settle` requires that many extra matching scans after a change is first
seen and defaults to 1, so one scan observes and the next confirms; zero
queues immediately. `capacity` bounds distinct pending paths and defaults to
1024. `clock` reads seconds from a monotonic origin and defaults to
`nupp.time`, and `source` reads one path's state and defaults to
`nupp.io.files.info`. The last two exist for a test or a headless tool that
drives its own time and its own filesystem; a game supplies neither.
### `Source` _type_
```nupp
type Source = function(path: string): (integer, number)
```
Reads one path's size and modification time.
A path that is missing, or that has become a directory, answers `-1, -1`
rather than raising: some editors save by renaming a temporary over the
target, so a scan landing in the gap has to read that as not settled.
## Functions
### `add` _function_
```nupp
function add(path: string, kind: string?): boolean
```
Registers a path to watch, and reports whether it was newly registered.
The path's current state becomes the baseline, so a file registered on frame
ten does not dispatch on frame eleven. Registering a path already watched
moves its kind and leaves its baseline alone, which is what a second loader
naming the same file should do.
Registration does not require an installed watcher. A loader registers what
it loaded whenever it loads it, and `install` takes the whole set up.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `path` | `string` | the file to watch |
| `kind` | `string?` | the content kind a change dispatches under, defaulting to `"document"` and to `"shader"` for a `.glsl` or `.wgsl` suffix |
#### Returns
| Type | Description |
| --- | --- |
| `boolean` | whether this call added the path rather than updating it |
#### Raises
- when path is empty
### `dispatch` _function_
```nupp
function dispatch(): integer
```
Runs the registered handlers over every queued change.
Call this once per logical update, at a point the frame has declared. A
handler that raises is logged and the batch continues, because one file that
is not usable yet says nothing about the next.
#### Returns
| Type | Description |
| --- | --- |
| `integer` | how many handlers ran and returned |
### `dispatched` _function_
```nupp
function dispatched(): integer
```
Returns how many changes have reached a handler since `install`.
#### Returns
| Type | Description |
| --- | --- |
| `integer` | the cumulative count of handler calls that returned |
### `forget` _function_
```nupp
function forget(path: string): boolean
```
Stops watching one path and forgets its state.
A change already queued for the path still dispatches, because it settled
while the path was watched and a handler that has been promised a reload
gets one.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `path` | `string` | the file to stop watching |
#### Returns
| Type | Description |
| --- | --- |
| `boolean` | whether the path was being watched |
### `install` _function_
```nupp
function install(config: Config?): nil
```
Starts watching, taking every registered path's current state as its
baseline.
Changes made before this call do not dispatch, which is what lets a build
register paths as it loads them and install once everything is loaded.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `config` | `Config?` | the polling options, or nil for the documented defaults |
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### Raises
- when settle is negative or capacity is not a positive integer
### `isInstalled` _function_
```nupp
function isInstalled(): boolean
```
Returns whether the watcher is running.
#### Returns
| Type | Description |
| --- | --- |
| `boolean` | true after `install` and before `uninstall` |
### `kinds` _function_
```nupp
function kinds(): {string}
```
Returns the content kinds that have a handler, in sorted order.
#### Returns
| Type | Description |
| --- | --- |
| `{string}` | a fresh caller-owned list |
### `on` _function_
```nupp
function on(kind: string, handler: ChangeHandler?): nil
```
Registers the handler for a content kind.
Re-registering a kind replaces its handler, and nil removes it. Handlers
outlive `install` and `uninstall`, so a build that stops and starts watching
registers them once.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `kind` | `string` | the content kind to register under |
| `handler` | `ChangeHandler?` | the handler to run, or nil to remove the current one |
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### Raises
- when kind is empty
### `pending` _function_
```nupp
function pending(): integer
```
Returns how many settled changes are waiting for `dispatch`.
#### Returns
| Type | Description |
| --- | --- |
| `integer` | the number of distinct queued paths |
### `poll` _function_
```nupp
function poll(): integer
```
Scans when the interval has elapsed, and does nothing otherwise.
This is what a host calls once per turn. It never dispatches, so it is safe
anywhere in a turn.
#### Returns
| Type | Description |
| --- | --- |
| `integer` | how many changes the scan queued, and zero when it was too early |
### `reset` _function_
```nupp
function reset(): nil
```
Forgets every path, handler and queued change, and stops watching.
A test that installs the watcher needs this between cases. A game does not:
`uninstall` already stops the polling and keeps what was registered.
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
### `scan` _function_
```nupp
function scan(): integer
```
Looks at every watched path once, whatever the interval says.
Call this directly when a headless tool or a test controls the timing. A
change that has now settled is queued rather than dispatched, because the
handler runs where the frame says it runs.
#### Returns
| Type | Description |
| --- | --- |
| `integer` | how many changes this scan queued, and zero when nothing is running |
### `uninstall` _function_
```nupp
function uninstall(): nil
```
Stops watching and drops whatever was queued.
Registered paths and handlers survive, so a later `install` resumes with the
same set and takes a fresh baseline.
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
### `unsettled` _function_
```nupp
function unsettled(): {string}
```
Returns the watched paths whose latest change has not settled, sorted.
#### Returns
| Type | Description |
| --- | --- |
| `{string}` | a fresh caller-owned list |
### `watching` _function_
```nupp
function watching(): {string}
```
Returns the watched paths, in sorted order.
#### Returns
| Type | Description |
| --- | --- |
| `{string}` | a fresh caller-owned list |
## Values
### `DOCUMENT` _variable_
```nupp
const DOCUMENT
```
Read-only. Names the kind a file registered without one is watched as.
### `SHADER` _variable_
```nupp
const SHADER
```
Read-only. Names the kind a shader source is watched as.