initial: notify v0.1.0 — tag-based player-message routing
Tag-based messages, channel registry, tag-filter routing (OR-list, empty=catch-all), per-channel history ring-buffer, subscriber callbacks, severity sugar wrappers (info/warn/error), and engine.print mirror.
This commit is contained in:
232
README.md
Normal file
232
README.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# lib-core.notify
|
||||
|
||||
Headless player-facing message routing. Tag-based messages with a channel
|
||||
registry, tag-filter routing, subscriber callbacks, and an engine.print mirror.
|
||||
No render code; no UI.
|
||||
|
||||
**Version:** 0.1.0
|
||||
**Lib-ID:** lib-core.notify
|
||||
**Requires:** engine.time.now (optional, for auto-timestamp), engine.print (optional, for log mirror)
|
||||
**Tags:** notify, message, channel, routing, hud
|
||||
|
||||
## Topology
|
||||
|
||||
<!-- topology:start (auto-generated; do not edit) -->
|
||||
```mermaid
|
||||
graph LR
|
||||
this["lib-core.notify"]
|
||||
engine_time["engine.time.now (optional)"]
|
||||
engine_print["engine.print (optional)"]
|
||||
this -.->|optional| engine_time
|
||||
this -.->|optional| engine_print
|
||||
```
|
||||
<!-- topology:end -->
|
||||
|
||||
## Scope (v0.1.0)
|
||||
|
||||
v0.1 ships the data layer only:
|
||||
|
||||
- Channel registry (create, destroy, list)
|
||||
- Tag-filter routing: OR-match on shared tags; empty filter = catch-all
|
||||
- Per-channel history ring-buffer (max_history capped, oldest dropped)
|
||||
- Subscribe / unsubscribe callbacks (returns opaque handle)
|
||||
- Severity sugar wrappers: `info`, `warn`, `error`
|
||||
- Engine-log mirror (calls `engine.print` on every posted message; toggle via `set_engine_log_mirror`)
|
||||
|
||||
**Intentional non-goals (deferred):**
|
||||
- Glob/wildcard tag filters
|
||||
- AND-combination or exclude-tag filters
|
||||
- Persistent history (disk/savegame serialization)
|
||||
- Display rendering (belongs in a future lib-core.notify-display)
|
||||
|
||||
## Message Schema
|
||||
|
||||
| Field | Type | Set by | Notes |
|
||||
|-------------|--------------|-------------------|--------------------------------|
|
||||
| `tags` | string[] | caller (required) | Non-empty; drives routing |
|
||||
| `text` | string | caller (required) | Human-readable message body |
|
||||
| `severity` | string | caller / auto | `"info"` / `"warn"` / `"error"`; defaults to `"info"` |
|
||||
| `id` | number | auto | Monotonically increasing |
|
||||
| `timestamp` | number / nil | auto | `engine.time.now()` if available |
|
||||
|
||||
## Channel Schema
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|----------------|----------|----------|----------------------------------------------------|
|
||||
| `id` | string | yes | Unique channel identifier |
|
||||
| `filter` | string[] | yes | Tag OR-list; `{}` = catch-all |
|
||||
| `display_mode` | string | yes | `"log"` / `"toast"` / `"detail"` |
|
||||
| `max_history` | number | no | Default: log=200, toast=20, detail=1 |
|
||||
| `title` | string | no | Human-readable channel name |
|
||||
|
||||
## Filter Semantics
|
||||
|
||||
Routing uses an OR-list: a message is delivered to a channel if at least one
|
||||
of the message's tags appears in the channel's filter array.
|
||||
|
||||
An empty filter array (`{}`) is a catch-all — every message is delivered
|
||||
regardless of its tags.
|
||||
|
||||
```
|
||||
channel filter = {"combat", "loot"} message tags = {"loot", "vendor"}
|
||||
→ match: "loot" in both lists
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### `notify.create_channel(def)`
|
||||
|
||||
**Syntax:** `notify.create_channel(def: table) -> void`
|
||||
|
||||
**Example:**
|
||||
```lua
|
||||
notify.create_channel{
|
||||
id = "combat-log",
|
||||
filter = {"combat", "damage"},
|
||||
display_mode = "log",
|
||||
max_history = 100,
|
||||
title = "Combat Log",
|
||||
}
|
||||
```
|
||||
|
||||
Registers a new channel. Loud-error on: non-table `def`, missing or empty
|
||||
`def.id`, duplicate id, non-table `def.filter`, unknown `def.display_mode`.
|
||||
`max_history` defaults to display-mode default (log=200, toast=20, detail=1).
|
||||
|
||||
---
|
||||
|
||||
### `notify.destroy_channel(channel_id)`
|
||||
|
||||
**Syntax:** `notify.destroy_channel(channel_id: string) -> void`
|
||||
|
||||
Removes the channel and all its subscribers. Idempotent (no-op if channel
|
||||
does not exist).
|
||||
|
||||
---
|
||||
|
||||
### `notify.list_channels()`
|
||||
|
||||
**Syntax:** `notify.list_channels() -> table[]`
|
||||
|
||||
Returns a shallow-copy array of all registered channel definitions (without
|
||||
internal history). Order is unspecified.
|
||||
|
||||
---
|
||||
|
||||
### `notify.post(msg)`
|
||||
|
||||
**Syntax:** `notify.post(msg: table) -> void`
|
||||
|
||||
**Example:**
|
||||
```lua
|
||||
notify.post{ tags = {"combat", "damage"}, text = "You hit goblin for 12 damage.", severity = "info" }
|
||||
```
|
||||
|
||||
Posts a message. Auto-fills `id`, `timestamp`, and `severity` (default
|
||||
`"info"`). Routes to all channels whose filter matches at least one tag.
|
||||
Appends to channel history (ring-buffered at `max_history`), invokes all
|
||||
registered subscriber callbacks in insertion order, and mirrors to
|
||||
`engine.print` if `engine_log_mirror` is enabled.
|
||||
|
||||
Loud-error on: non-table `msg`, missing or non-array `msg.tags`, empty
|
||||
`msg.tags`, non-string tag elements, non-string `msg.text`.
|
||||
|
||||
---
|
||||
|
||||
### `notify.info(tags, text)`
|
||||
|
||||
**Syntax:** `notify.info(tags: string[], text: string) -> void`
|
||||
|
||||
Sugar for `M.post{ tags=tags, text=text, severity="info" }`.
|
||||
|
||||
---
|
||||
|
||||
### `notify.warn(tags, text)`
|
||||
|
||||
**Syntax:** `notify.warn(tags: string[], text: string) -> void`
|
||||
|
||||
Sugar for `M.post{ tags=tags, text=text, severity="warn" }`.
|
||||
|
||||
---
|
||||
|
||||
### `notify.error(tags, text)`
|
||||
|
||||
**Syntax:** `notify.error(tags: string[], text: string) -> void`
|
||||
|
||||
Sugar for `M.post{ tags=tags, text=text, severity="error" }`.
|
||||
|
||||
---
|
||||
|
||||
### `notify.get_history(channel_id, n)`
|
||||
|
||||
**Syntax:** `notify.get_history(channel_id: string, n?: number) -> table[]`
|
||||
|
||||
**Example:**
|
||||
```lua
|
||||
local last5 = notify.get_history("combat-log", 5)
|
||||
```
|
||||
|
||||
Returns a shallow-copy array of the last `n` messages delivered to the
|
||||
channel, oldest-first within the returned slice. If `n` is nil or greater
|
||||
than the number of stored messages, returns all stored messages. Returns
|
||||
`{}` for unknown channels.
|
||||
|
||||
---
|
||||
|
||||
### `notify.subscribe(channel_id, callback)`
|
||||
|
||||
**Syntax:** `notify.subscribe(channel_id: string, callback: function) -> handle`
|
||||
|
||||
**Example:**
|
||||
```lua
|
||||
local handle = notify.subscribe("combat-log", function(msg)
|
||||
hud.append(msg.text)
|
||||
end)
|
||||
```
|
||||
|
||||
Registers `callback` to be invoked whenever a message is routed to
|
||||
`channel_id`. Returns an opaque handle table required by `unsubscribe`.
|
||||
Loud-error on unknown channel or non-function callback.
|
||||
|
||||
---
|
||||
|
||||
### `notify.unsubscribe(handle)`
|
||||
|
||||
**Syntax:** `notify.unsubscribe(handle: table) -> void`
|
||||
|
||||
Removes the subscription identified by `handle`. Silent if the subscription
|
||||
or channel no longer exists (idempotent).
|
||||
|
||||
---
|
||||
|
||||
### `notify.set_engine_log_mirror(enable)`
|
||||
|
||||
**Syntax:** `notify.set_engine_log_mirror(enable: bool) -> void`
|
||||
|
||||
Enables or disables mirroring every posted message to `engine.print`.
|
||||
Only `true` (boolean) enables the mirror; any other value disables it.
|
||||
Enabled by default at module load.
|
||||
|
||||
---
|
||||
|
||||
## Engine-Log Mirror
|
||||
|
||||
When `engine_log_mirror` is enabled (default), every call to `notify.post`
|
||||
also calls `engine.print(msg.text)` if `engine.print` is available. This
|
||||
provides zero-config visibility in the engine console during development.
|
||||
|
||||
Disable in production or when a channel subscriber handles its own output:
|
||||
|
||||
```lua
|
||||
notify.set_engine_log_mirror(false)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Backdoors
|
||||
|
||||
The following functions are for test-only use. Do not call them from game code.
|
||||
|
||||
- `notify._test_clear_all()` — resets all state to initial values
|
||||
- `notify._test_get_subscribers(channel_id)` — returns raw subscriber list
|
||||
- `notify._test_get_log_mirror()` — returns current `engine_log_mirror` bool
|
||||
Reference in New Issue
Block a user