tecs.watch
Watches content for changes and hands settled ones to reload handlers.
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.
Module contents
Types
| Type | Kind | Description |
|---|---|---|
Change | record | One settled content change. |
ChangeHandler | type | Receives one settled change. |
Config | type | Settings copied by install. |
Source | type | Reads one path's size and modification time. |
Functions
| Function | Kind | Description |
|---|---|---|
add | function | Registers a path to watch, and reports whether it was newly registered. |
dispatch | function | Runs the registered handlers over every queued change. |
dispatched | function | Returns how many changes have reached a handler since install. |
forget | function | Stops watching one path and forgets its state. |
install | function | Starts watching, taking every registered path's current state as its baseline. |
isInstalled | function | Returns whether the watcher is running. |
kinds | function | Returns the content kinds that have a handler, in sorted order. |
on | function | Registers the handler for a content kind. |
pending | function | Returns how many settled changes are waiting for dispatch. |
poll | function | Scans when the interval has elapsed, and does nothing otherwise. |
reset | function | Forgets every path, handler and queued change, and stops watching. |
scan | function | Looks at every watched path once, whatever the interval says. |
uninstall | function | Stops watching and drops whatever was queued. |
unsettled | function | Returns the watched paths whose latest change has not settled, sorted. |
watching | function | Returns the watched paths, in sorted order. |
Values
| Value | Kind | Description |
|---|---|---|
DOCUMENT | variable | Read-only. |
SHADER | variable | Read-only. |
Types#
Changerecord#
One settled content change.
Fields
kind#
kind: stringRead-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.
ChangeHandlertype#
type ChangeHandler = function(change: Change): nilReceives one settled change.
Configtype#
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.
Sourcetype#
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#
addfunction#
function add(path: string, kind: string?): booleanRegisters 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 |
Returns
| Type | Description |
|---|---|
boolean | whether this call added the path rather than updating it |
Raises
when path is empty
dispatchfunction#
function dispatch(): integerRuns 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 |
dispatchedfunction#
function dispatched(): integerReturns how many changes have reached a handler since install.
Returns
| Type | Description |
|---|---|
integer | the cumulative count of handler calls that returned |
forgetfunction#
function forget(path: string): booleanStops 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 |
installfunction#
function install(config: Config?): nilStarts 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
isInstalledfunction#
function isInstalled(): booleanReturns whether the watcher is running.
Returns
| Type | Description |
|---|---|
boolean | true after |
kindsfunction#
function kinds(): {string}Returns the content kinds that have a handler, in sorted order.
Returns
| Type | Description |
|---|---|
{string} | a fresh caller-owned list |
onfunction#
function on(kind: string, handler: ChangeHandler?): nilRegisters 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
pendingfunction#
function pending(): integerReturns how many settled changes are waiting for dispatch.
Returns
| Type | Description |
|---|---|
integer | the number of distinct queued paths |
pollfunction#
function poll(): integerScans 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 |
resetfunction#
function reset(): nilForgets 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 |
scanfunction#
function scan(): integerLooks 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 |
uninstallfunction#
function uninstall(): nilStops 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 |
unsettledfunction#
function unsettled(): {string}Returns the watched paths whose latest change has not settled, sorted.
Returns
| Type | Description |
|---|---|
{string} | a fresh caller-owned list |
watchingfunction#
function watching(): {string}Returns the watched paths, in sorted order.
Returns
| Type | Description |
|---|---|
{string} | a fresh caller-owned list |
Values#
DOCUMENTvariable#
const DOCUMENTRead-only. Names the kind a file registered without one is watched as.
SHADERvariable#
const SHADERRead-only. Names the kind a shader source is watched as.