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.
148 lines
5.3 KiB
Markdown
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.
|