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:
24
LICENSE
Normal file
24
LICENSE
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Copyright (c) 2026 Calic. All rights reserved.
|
||||||
|
|
||||||
|
This software is part of the Sporel platform — **Tier 1 (Official /
|
||||||
|
Proprietary)** content per the Three-Tier Licensing Model documented in
|
||||||
|
`meta/docs/archive/design/vision.md §Licensing Model` (current source;
|
||||||
|
migration to `meta/docs/architecture/licensing-model.md` pending).
|
||||||
|
|
||||||
|
⚠ **WIP — Legal review required before public launch.** The terms below
|
||||||
|
reflect design intent only; the formalized license framework will be
|
||||||
|
finalized through legal counsel before the first public release. Until
|
||||||
|
then, this notice serves as a placeholder defending the platform owner's
|
||||||
|
rights against unintentional re-licensing.
|
||||||
|
|
||||||
|
No license is granted to copy, modify, distribute, sublicense, or otherwise
|
||||||
|
use this software in any form without prior written permission from the
|
||||||
|
copyright holder.
|
||||||
|
|
||||||
|
References:
|
||||||
|
- Tier 1 (this file): all rights reserved, proprietary, sold/distributed
|
||||||
|
via official channels (Steam, etc.)
|
||||||
|
- Tier 2 (Semi-Commercial Co-Development): bilateral contracts, revenue-
|
||||||
|
share — see vision.md §Licensing Model
|
||||||
|
- Tier 3 (Community Content): CC BY-NC-SA 4.0 + asymmetric CLA — applies
|
||||||
|
to community-uploaded libs/modules/assets, not this repo
|
||||||
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
|
||||||
267
init.lua
Normal file
267
init.lua
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
-- =====================================================================
|
||||||
|
-- lib-core.notify v0.1.0 — Player-Facing Message Routing
|
||||||
|
-- Tag-based messages + channel registry + tag-filter routing.
|
||||||
|
-- Spec: meta/docs/superpowers/specs/2026-06-14-notification-system-design.md
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
-- Module-table early so module-local closures can reference M.*
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
-- ---------- module state (all local) ----------
|
||||||
|
local channels = {} -- channel_id → channel_def
|
||||||
|
local subscribers = {} -- channel_id → array of {callback, sub_id}
|
||||||
|
local next_msg_id = 1
|
||||||
|
local next_sub_id = 1
|
||||||
|
local engine_log_mirror = true
|
||||||
|
|
||||||
|
-- Valid display modes (capability-by-declaration)
|
||||||
|
local VALID_DISPLAY_MODES = {
|
||||||
|
log = true,
|
||||||
|
toast = true,
|
||||||
|
detail = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Default max_history per display_mode
|
||||||
|
local DEFAULT_MAX_HISTORY = {
|
||||||
|
log = 200,
|
||||||
|
toast = 20,
|
||||||
|
detail = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- -----------------------------------------------------------------------
|
||||||
|
-- Internal helpers
|
||||||
|
-- -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function tags_match(channel_filter, msg_tags)
|
||||||
|
-- Catch-all: empty filter matches everything
|
||||||
|
if #channel_filter == 0 then return true end
|
||||||
|
-- OR-list: ≥1 message-tag in channel_filter
|
||||||
|
for _, mtag in ipairs(msg_tags) do
|
||||||
|
for _, ftag in ipairs(channel_filter) do
|
||||||
|
if mtag == ftag then return true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function append_to_history(channel, msg)
|
||||||
|
table.insert(channel.history, msg)
|
||||||
|
while #channel.history > channel.max_history do
|
||||||
|
table.remove(channel.history, 1) -- drop oldest
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- -----------------------------------------------------------------------
|
||||||
|
-- Channel management
|
||||||
|
-- -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
function M.create_channel(def)
|
||||||
|
if type(def) ~= "table" then
|
||||||
|
error("notify.create_channel: def must be a table")
|
||||||
|
end
|
||||||
|
if type(def.id) ~= "string" or def.id == "" then
|
||||||
|
error("notify.create_channel: def.id must be a non-empty string")
|
||||||
|
end
|
||||||
|
local id = def.id
|
||||||
|
if channels[id] then
|
||||||
|
error("notify.create_channel '" .. id .. "': duplicate channel id")
|
||||||
|
end
|
||||||
|
if type(def.filter) ~= "table" then
|
||||||
|
error("notify.create_channel '" .. id .. "': def.filter must be a table (array)")
|
||||||
|
end
|
||||||
|
if not VALID_DISPLAY_MODES[def.display_mode] then
|
||||||
|
error("notify.create_channel '" .. id .. "': unknown display_mode '" ..
|
||||||
|
tostring(def.display_mode) .. "'; valid: log, toast, detail")
|
||||||
|
end
|
||||||
|
local max_history = def.max_history
|
||||||
|
if max_history == nil then
|
||||||
|
max_history = DEFAULT_MAX_HISTORY[def.display_mode]
|
||||||
|
end
|
||||||
|
channels[id] = {
|
||||||
|
id = id,
|
||||||
|
filter = def.filter,
|
||||||
|
display_mode = def.display_mode,
|
||||||
|
max_history = max_history,
|
||||||
|
title = def.title,
|
||||||
|
history = {},
|
||||||
|
}
|
||||||
|
subscribers[id] = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.destroy_channel(channel_id)
|
||||||
|
channels[channel_id] = nil
|
||||||
|
subscribers[channel_id] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.list_channels()
|
||||||
|
local result = {}
|
||||||
|
for _, ch in pairs(channels) do
|
||||||
|
-- shallow copy, omit internal history
|
||||||
|
result[#result + 1] = {
|
||||||
|
id = ch.id,
|
||||||
|
filter = ch.filter,
|
||||||
|
display_mode = ch.display_mode,
|
||||||
|
max_history = ch.max_history,
|
||||||
|
title = ch.title,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- -----------------------------------------------------------------------
|
||||||
|
-- Post
|
||||||
|
-- -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
function M.post(msg)
|
||||||
|
if type(msg) ~= "table" then
|
||||||
|
error("notify.post: msg must be a table")
|
||||||
|
end
|
||||||
|
if type(msg.tags) ~= "table" then
|
||||||
|
error("notify.post: msg.tags must be an array of strings")
|
||||||
|
end
|
||||||
|
if #msg.tags == 0 then
|
||||||
|
error("notify.post: msg.tags must be a non-empty array")
|
||||||
|
end
|
||||||
|
for i, tag in ipairs(msg.tags) do
|
||||||
|
if type(tag) ~= "string" then
|
||||||
|
error("notify.post: msg.tags[" .. i .. "] must be a string, got " .. type(tag))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if type(msg.text) ~= "string" then
|
||||||
|
error("notify.post: msg.text must be a string")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Shallow copy so caller's table is never mutated and history
|
||||||
|
-- entries can't alias across calls.
|
||||||
|
-- Note: tags and data are shared references; caller must not mutate
|
||||||
|
-- them after post().
|
||||||
|
local stored = {
|
||||||
|
tags = msg.tags,
|
||||||
|
text = msg.text,
|
||||||
|
severity = msg.severity or "info",
|
||||||
|
ttl = msg.ttl,
|
||||||
|
source = msg.source,
|
||||||
|
data = msg.data,
|
||||||
|
}
|
||||||
|
stored.id = next_msg_id
|
||||||
|
next_msg_id = next_msg_id + 1
|
||||||
|
stored.timestamp = (engine and engine.time and engine.time.now and engine.time.now()) or 0
|
||||||
|
|
||||||
|
-- Route to matching channels
|
||||||
|
for _, ch in pairs(channels) do
|
||||||
|
if tags_match(ch.filter, msg.tags) then
|
||||||
|
append_to_history(ch, stored)
|
||||||
|
for _, sub in ipairs(subscribers[ch.id]) do
|
||||||
|
sub.callback(stored)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Engine-log mirror
|
||||||
|
if engine_log_mirror and engine and engine.print then
|
||||||
|
engine.print(stored.text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- -----------------------------------------------------------------------
|
||||||
|
-- Sugar wrappers
|
||||||
|
-- -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
function M.info(tags, text)
|
||||||
|
M.post{ tags = tags, text = text, severity = "info" }
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.warn(tags, text)
|
||||||
|
M.post{ tags = tags, text = text, severity = "warn" }
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.error(tags, text)
|
||||||
|
M.post{ tags = tags, text = text, severity = "error" }
|
||||||
|
end
|
||||||
|
|
||||||
|
-- -----------------------------------------------------------------------
|
||||||
|
-- History
|
||||||
|
-- -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
function M.get_history(channel_id, n)
|
||||||
|
local ch = channels[channel_id]
|
||||||
|
if not ch then return {} end
|
||||||
|
local history = ch.history
|
||||||
|
local total = #history
|
||||||
|
if n == nil or n >= total then
|
||||||
|
-- Return shallow copy of all
|
||||||
|
local copy = {}
|
||||||
|
for i = 1, total do copy[i] = history[i] end
|
||||||
|
return copy
|
||||||
|
end
|
||||||
|
-- Return last n entries (oldest-first within slice)
|
||||||
|
local copy = {}
|
||||||
|
local start = total - n + 1
|
||||||
|
for i = start, total do
|
||||||
|
copy[#copy + 1] = history[i]
|
||||||
|
end
|
||||||
|
return copy
|
||||||
|
end
|
||||||
|
|
||||||
|
-- -----------------------------------------------------------------------
|
||||||
|
-- Subscribe / Unsubscribe
|
||||||
|
-- -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
function M.subscribe(channel_id, callback)
|
||||||
|
if not channels[channel_id] then
|
||||||
|
error("notify.subscribe: unknown channel '" .. tostring(channel_id) .. "'")
|
||||||
|
end
|
||||||
|
if type(callback) ~= "function" then
|
||||||
|
error("notify.subscribe: callback for channel '" .. tostring(channel_id) .. "' must be a function")
|
||||||
|
end
|
||||||
|
local sub_id = next_sub_id
|
||||||
|
next_sub_id = next_sub_id + 1
|
||||||
|
local entry = { callback = callback, sub_id = sub_id }
|
||||||
|
table.insert(subscribers[channel_id], entry)
|
||||||
|
return { channel_id = channel_id, sub_id = sub_id }
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.unsubscribe(handle)
|
||||||
|
if type(handle) ~= "table" or handle.channel_id == nil then
|
||||||
|
error("notify.unsubscribe: handle must be a table with channel_id field")
|
||||||
|
end
|
||||||
|
local list = subscribers[handle.channel_id]
|
||||||
|
if not list then return end -- channel destroyed, silent
|
||||||
|
for i, sub in ipairs(list) do
|
||||||
|
if sub.sub_id == handle.sub_id then
|
||||||
|
table.remove(list, i)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Not found: silent (idempotent)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- -----------------------------------------------------------------------
|
||||||
|
-- Engine-log mirror toggle
|
||||||
|
-- -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
function M.set_engine_log_mirror(enable)
|
||||||
|
engine_log_mirror = enable == true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- -----------------------------------------------------------------------
|
||||||
|
-- Test backdoors
|
||||||
|
-- -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
function M._test_clear_all()
|
||||||
|
channels = {}
|
||||||
|
subscribers = {}
|
||||||
|
next_msg_id = 1
|
||||||
|
next_sub_id = 1
|
||||||
|
engine_log_mirror = true
|
||||||
|
end
|
||||||
|
|
||||||
|
function M._test_get_subscribers(channel_id)
|
||||||
|
return subscribers[channel_id] or {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function M._test_get_log_mirror()
|
||||||
|
return engine_log_mirror
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
1
manifest.lib
Normal file
1
manifest.lib
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"id":"lib-core.notify","version":"0.1.0","api_min":"0.1","deps":[]}
|
||||||
Reference in New Issue
Block a user