Verbatim port of Fastway-Server's TFWEventBus from fw_plugin_host.pas
per feedback_copy_dont_reinterpret.md. Adjustments limited to:
- Type renames (TFW* -> T*).
- uses clause: drop fw_log; add log.types from fpc-log so the
optional Logger property uses the canonical ecosystem-wide
TLogProc shape, matching every other fpc-* library.
- Per-handler exception logging now calls Logger with
Level=llError, Category='events', and includes the source
plugin (ASourcePlugin parameter) in the message text so the
canonical signature stays meaningful.
Behaviours preserved verbatim: APluginName bulk-Unsubscribe key,
wildcard '*' subscriber, OnBroadcast external-listener tap,
snapshot-iterate-outside-lock pattern, per-handler exception
isolation, TCriticalSection.
docs/DEVELOPER_GUIDE.md added covering threading, payload
ownership, recursive Fire, OnBroadcast, logger plumbing, and
the relationship between fpc-events (ecosystem-wide pub/sub)
and per-library typed observer callbacks (bp.events / cm.events
pattern).
Tests: 44 assertions across 14 scenarios pass on x86_64-linux.
Pre-tag -vh audit on src/ev.bus.pas reports zero hints/warnings.
147 lines
6.3 KiB
Markdown
147 lines
6.3 KiB
Markdown
# fpc-events — architecture
|
|
|
|
## Layers
|
|
|
|
```
|
|
┌──────────────────────────────────────────────────┐
|
|
│ Caller (BBS, plugin, daemon, web-admin, …) │
|
|
└──────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌──────────────────────────────────────────────────┐
|
|
│ events.bus (TEventBus class) │
|
|
├──────────────────────────────────────────────────┤
|
|
│ events.version (semver constants) │
|
|
├──────────────────────────────────────────────────┤
|
|
│ log.types (logger callback type only, │
|
|
│ from sibling fpc-log) │
|
|
├──────────────────────────────────────────────────┤
|
|
│ RTL: Classes (TCriticalSection), fpjson │
|
|
└──────────────────────────────────────────────────┘
|
|
```
|
|
|
|
Two source units, no platform-specific code. No backends, no
|
|
formats, no I/O — `TEventBus` is a pure in-memory pub/sub
|
|
registry.
|
|
|
|
## Origin
|
|
|
|
Verbatim port of `TFWEventBus` from Fastway-Server's
|
|
`fw_plugin_host.pas` (lines 188-203 + 1626-1753). Preserved
|
|
behaviours:
|
|
|
|
- `APluginName` parameter on `Subscribe` + bulk-Unsubscribe key.
|
|
- Wildcard `'*'` event type.
|
|
- `OnBroadcast` external-listener tap.
|
|
- Snapshot-iterate-outside-lock (safe re/un-subscribe + recursive
|
|
`Fire` from inside callbacks).
|
|
- Per-handler exception isolation via `try/except` *inside* the
|
|
iteration.
|
|
- `TCriticalSection` synchronisation (not `TMonitor` / RW lock).
|
|
|
|
The only structural change: `fw_log.Log.Error(...)` calls in the
|
|
canonical handler-error path were replaced with the optional
|
|
`Logger: log.types.TLogProc` callback to drop the Fastway-only
|
|
`fw_log` dependency.
|
|
|
|
## Where it fits in the fpc-* family
|
|
|
|
```
|
|
┌────────────────────────┐ ┌────────────────────────┐
|
|
│ fpc-events │ │ fpc-binkp / fpc-comet │
|
|
│ ecosystem-wide │ │ / fpc-emsi │
|
|
│ pub/sub │ │ per-library typed │
|
|
│ (one bus, N pubs, │ │ observer callbacks │
|
|
│ M subs) │ │ (one publisher, N │
|
|
│ │ │ consumers) │
|
|
└────────────────────────┘ └────────────────────────┘
|
|
▲ ▲
|
|
│ │
|
|
└────────── pick one ────────┘
|
|
(or use both — they're orthogonal)
|
|
```
|
|
|
|
These are different patterns for different problems:
|
|
|
|
- **Per-library typed callbacks** — `Session.OnFileBegin :=
|
|
@MyHandler` on a fpc-binkp session. Type-safe, no JSON
|
|
marshalling, the publisher knows its consumers. Used by
|
|
fpc-binkp's `bp.events`, fpc-comet's `cm.events`, fpc-cron's
|
|
`cron.events`.
|
|
|
|
- **fpc-events** — a single `TEventBus` instance receives
|
|
`Fire(...)` calls from many publishers; many `Subscribe(...)`d
|
|
consumers (web-admin WebSocket, audit log, monitoring) listen
|
|
without each publisher knowing they exist. Cost: `AData` is
|
|
JSON-bagged (no static typing); event names are strings.
|
|
|
|
A common hybrid: a library exposes typed callbacks AND a small
|
|
bridge class (consumer-written) that forwards selected events to
|
|
a `TEventBus`. fpc-events doesn't impose either pattern — it's
|
|
the substrate when the second pattern is what you need.
|
|
|
|
## Threading model
|
|
|
|
Single `TCriticalSection` (`FLock`) protects `FSubscriptions`.
|
|
|
|
- `Subscribe`, `Unsubscribe`, `UnsubscribeCallback`,
|
|
`GetSubscriptionCount` — acquire lock, mutate or read, release.
|
|
- `Fire` — acquire lock, copy `FSubscriptions` to a local array,
|
|
release lock, then iterate the copy *outside the lock*.
|
|
|
|
The snapshot-iterate pattern is load-bearing: a callback that
|
|
calls `Subscribe` / `Unsubscribe` / `Fire` recursively does so
|
|
without contending for the lock, and its modifications don't
|
|
affect the in-flight delivery (the snapshot was taken before the
|
|
mutation).
|
|
|
|
The bus does no thread marshalling. Subscribers run on whichever
|
|
thread called `Fire`. Consumers wanting cross-thread dispatch
|
|
(UI updates, message-queue posting) do that themselves inside
|
|
the handler.
|
|
|
|
## Memory ownership
|
|
|
|
The `AData: TJSONObject` parameter on `Fire` is **owned by the
|
|
caller**. Subscribers may read fields and copy values out, but
|
|
must not call `AData.Free`.
|
|
|
|
Standard publisher pattern:
|
|
|
|
```pascal
|
|
Data := TJSONObject.Create;
|
|
try
|
|
Data.Add('user_id', 42);
|
|
Bus.Fire('auth', 'user.login', Data);
|
|
finally
|
|
Data.Free;
|
|
end;
|
|
```
|
|
|
|
If a subscriber needs to retain the data beyond the `Fire` call,
|
|
it must clone:
|
|
|
|
```pascal
|
|
FRetained := TJSONObject(AData.Clone);
|
|
```
|
|
|
|
## Logger plumbing
|
|
|
|
`Bus.Logger: TLogProc` (from `log.types`) handles the
|
|
exception-isolation log path. When a subscriber callback or the
|
|
`OnBroadcast` tap raises:
|
|
|
|
- The exception is caught by `Fire`'s inner `try/except`.
|
|
- If `Logger` is assigned, it's called with
|
|
`(llError, 'events', formatted-message)`.
|
|
- The exception is then swallowed; delivery continues to the
|
|
next subscriber.
|
|
|
|
If `Logger` is `nil` (the default), exceptions are silently
|
|
swallowed. This matches canonical Fastway behaviour for callers
|
|
without an `fw_log.Log` configured.
|
|
|
|
`TLogProc` is defined in fpc-log's `log.types` — the same shape
|
|
fpc-binkp, fpc-comet, fpc-emsi, fpc-cron use. One log sink wires
|
|
to all of them.
|