# `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.