Files
kenjreno 128ddcb4df v0.1.0: thread-safe pub/sub event bus
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.
2026-05-05 18:13:10 -07:00

148 lines
5.3 KiB
Markdown

# fpc-events
Thread-safe publish/subscribe event bus for Free Pascal. Verbatim
port of the `TFWEventBus` primitive from Fastway-Server's
`fw_plugin_host.pas` — same surface, same semantics, no
Fastway-specific dependencies.
## What it is
- One class, `TEventBus`, that delivers `(EventType, JSONObject)`
payloads to registered subscribers.
- Subscribers are method-of-object callbacks; they can be removed
in bulk by group key (`Unsubscribe(plugin_name)`) or one-by-one
by callback identity (`UnsubscribeCallback`).
- Wildcard subscribers (`'*'`) receive every event fired.
- A single optional `OnBroadcast` tap fires once per `Fire` call,
for forwarding events out of band (the original use case is
pushing events to web-admin WebSocket clients).
- Per-handler exceptions are isolated — a raising subscriber does
not block delivery to the others, and the optional `Logger`
callback receives the exception text.
- Snapshot-iterate semantics: a callback can safely
Subscribe / Unsubscribe / Fire recursively without deadlocking
the registry.
## Quick start
```pascal
uses
Classes, SysUtils, fpjson, events.bus;
type
TListener = class
procedure HandleLogin(const AEventType: string; AData: TJSONObject);
end;
procedure TListener.HandleLogin(const AEventType: string; AData: TJSONObject);
begin
Writeln('user.login: ', AData.Get('username', '?'));
end;
var
Bus: TEventBus;
L: TListener;
Data: TJSONObject;
begin
Bus := TEventBus.Create;
L := TListener.Create;
try
Bus.Subscribe('demo', 'user.login', @L.HandleLogin);
Data := TJSONObject.Create;
try
Data.Add('username', 'alice');
Bus.Fire('auth', 'user.login', Data);
finally
Data.Free;
end;
finally
L.Free;
Bus.Free;
end;
end.
```
See `examples/pubsub.pas` for a more complete demo with wildcard
subscribers and bulk unsubscribe, and
[`docs/DEVELOPER_GUIDE.md`](docs/DEVELOPER_GUIDE.md) for the
full developer guide covering threading, payload ownership,
recursive Fire, and the relationship with per-library typed
callbacks.
## API surface
| Method / property | Purpose |
|---|---|
| `Create` | Construct an empty bus. |
| `Subscribe(APluginName, AEventType, ACallback)` | Register a callback for `AEventType`. `'*'` matches every event. `APluginName` is a free-form group key for bulk removal; pass `''` if unused. |
| `Unsubscribe(APluginName)` | Remove every subscription whose `PluginName` matches (case-insensitive). |
| `UnsubscribeCallback(ACallback)` | Remove every subscription whose method pointer (Code+Data) matches. |
| `Fire(ASourcePlugin, AEventType, AData)` | Deliver `AEventType+AData` to every matching subscriber, then fire `OnBroadcast` if assigned. Caller retains ownership of `AData`. |
| `GetSubscriptionCount` | Number of registered subscriptions. |
| `OnBroadcast: TEventBroadcast` | External-listener tap fired once per `Fire`. |
| `Logger: log.types.TLogProc` | Optional sink for per-handler exception messages. `nil` = silent. Same shape as every other fpc-* library. |
### Types
- `TEventCallback = procedure(const AEventType: string; AData: TJSONObject) of object;`
- `TEventBroadcast = procedure(const AEventType: string; AData: TJSONObject) of object;`
- `Logger`-property type is `log.types.TLogProc` (from fpc-log) — the
ecosystem-wide logger shape.
## Dependencies
- [fpc-log](../fpc-log) — only `log.types.TLogProc` for the
optional `Logger` property.
### Notes on behaviour
- `Subscribe` does not deduplicate. Registering the same callback
twice means it fires twice.
- `Fire`'s subscription delivery order matches insertion order.
- `AData` ownership stays with the caller of `Fire`; subscribers
must not free it. The most common pattern is
`Data := TJSONObject.Create; try Bus.Fire(...); finally Data.Free; end;`.
- The `ASourcePlugin` parameter on `Fire` is currently passthrough
metadata — the bus itself does not read it. The original
Fastway use was provenance tagging at the call site; consumers
can ignore it.
- All operations are thread-safe via a single `TCriticalSection`.
`Fire` snapshots subscriptions under the lock and iterates
outside it, so re/un-subscribe and recursive `Fire` from inside
a callback are safe.
## Build and test
```bash
bash build.sh # compile every src/*.pas
bash run_tests.sh # build + run every tests/test_*.pas, compile every example
```
`build.sh` is single-target (`x86_64-linux`). Cross-compilation
is the consumer's responsibility — `events.version.pas` and `events.bus.pas`
are pure FPC + RTL (`Classes`, `SysUtils`, `fpjson`, `syncobjs`)
and have no platform-specific code.
## Versioning
Pin downstream consumers by tag (`v0.1.0`), not commit hash.
Constants in `src/events.version.pas`:
```
EVENTS_VERSION_MAJOR = 0;
EVENTS_VERSION_MINOR = 1;
EVENTS_VERSION_PATCH = 0;
EVENTS_VERSION_STRING = '0.1.0';
```
Semver intent:
- **major** — breaks callers (API removal, signature change)
- **minor** — additive features
- **patch** — bug fixes, internal cleanups
## License
Same as Fastway-Server: MIT (see LICENSE if/when added by the
consumer). Until then, treat the source as Fastway-Server-licensed.