Ken Johnson 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
2026-05-05 18:13:10 -07:00
2026-05-05 18:13:10 -07:00
2026-05-05 18:13:10 -07:00

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

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 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 — 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 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.

Description
Thread-safe pub/sub event bus for Free Pascal
Readme 53 KiB
Languages
Pascal 91.7%
Shell 8.3%