#Posoco events

    @events owns the open Event type and the owner-bound Events capability. Posoco publishes core notifications as Core(TurnEvent). A community protocol package adds its own constructor and keeps a closed payload ADT, so consumers pattern match with static types: no JSON codec, no downcast, no runtime registry.

    ///|
    pub(all) enum SprintEvent {
    Started(Int32)
    Finished
    }

    ///|
    pub(all) extenum @events.Event += {
    SprintChanged(SprintEvent)
    }

    ///|
    test "a community protocol extends the open event type" {
    let event : @events.Event = SprintChanged(Started(3))
    let described = match event {
    SprintChanged(Started(sprint)) => "started #\{sprint}"
    SprintChanged(Finished) => "finished"
    @events.Core(_) => "core notification"
    _ => "another protocol"
    }
    assert_eq(described, "started #3")
    }

    A capability publishes only in the namespace the Agent granted it (an extension's manifest id) while subscribe may listen to any namespace. Handlers are synchronous and non-raising; cancel is idempotent and a new listener receives no history.

    Real extensions declare Capability::Events and obtain their capability in Lifecycle::on_compose; hosts use Agent::subscribe_events:

    1. Define an ADT and extend @posoco.Event.
    2. Declare requires: [@posoco.Capability::Events] and lifecycle: [self] in the extension manifest.
    3. Obtain view.events() in Lifecycle::on_compose; publish in on_start or a business method, not during construction.
    4. Subscribe with events.subscribe (extensions) or agent.subscribe_events (hosts), optionally filtering with namespace_=Some(Extension(id)) or namespace_=Some(Posoco). None means all namespaces.
    5. Handle the Result, retain the subscription, and call cancel() when done. After shutdown, new publications/subscriptions return Err(Closed); an extension attempting to publish Core returns Err(ReservedEvent).

    The manifest id binds publishing identity and must be unique for Events users. Posoco is reserved for core notifications, not an extra bus. Observer is a core-only adapter on the same dispatcher; Hook remains interception, and awaited journal/checkpoint commits remain separate from notifications.

    For reusable protocols, export the payload ADT and constructor from a protocol package. The root @posoco.Event facade can be extended, but an extenum constructor must be qualified by its defining package, e.g. @events.Core or @protocol.SprintChanged, not by a package that only re-exports the type. Keep a wildcard for the open Event; exhaustively match the closed payload.

    Delivery uses registration order and a per-batch listener snapshot; reentrant publications queue rather than recurse. Changes to subscriptions during a batch apply to the next batch. Use immutable community payloads: they are not deep copied. scope is optional delivery attribution, not a routing filter.

    The following isolated test uses fake callbacks only to illustrate the listener shape; from_callbacks and envelope construction are framework/test seams, not steps a product extension needs to perform.

    ///|
    test "subscribe to typed deliveries and cancel" {
    let delivered : Array[String] = []
    let dispatch : Ref[(@events.EventEnvelope) -> Unit] = Ref(fn(_) { () })
    let events = @events.Events::from_callbacks(
    publish=fn(_event, _scope) { Ok(()) },
    subscribe=fn(handler, _namespace_) {
    // A real runtime records the handler and filters by namespace.
    dispatch.val = handler
    Ok(@events.Subscription::from_cancel(fn() { () }))
    },
    )
    let subscription = events.subscribe(fn(envelope) {
    match envelope.event {
    SprintChanged(Started(sprint)) =>
    // The envelope namespace is system-produced delivery metadata.
    match envelope.namespace_of() {
    Extension("sprint") => delivered.push("sprint started #\{sprint}")
    _ => delivered.push("unexpected namespace")
    }
    _ => ()
    }
    })
    guard subscription is Ok(handle) else { fail("expected a subscription") }
    (dispatch.val)(
    @events.EventEnvelope(
    namespace_=@events.Extension("sprint"),
    event=SprintChanged(Started(4)),
    ),
    )
    handle.cancel()
    handle.cancel()
    assert_eq(delivered, ["sprint started #4"])
    }

    Event

    pub extenum Event {
    }

    The notification value listeners receive. Posoco owns the core wrapper; a community protocol package adds its own constructor:

    pub(all) extenum @events.Event += {
    SprintChanged(SprintEvent)
    }

    Consumers import the protocol package and pattern match the payload with its static type — no JSON codec, downcast or runtime registry. Matching on the open type needs a wildcard branch; the business ADT stays exhaustive.

    A community payload is a value, not a serialization boundary: publish it as immutable data. Posoco snapshots core TurnEvent payloads but does not deep-copy arbitrary ADTs.

    EventEnvelope

    pub struct EventEnvelope {
    namespace_ : EventNamespace
    scope :
    EventScope
    ?
    event : Event
    }

    One delivered event with the metadata the system produced for it. Subscribers read namespace_, scope and event; they never author delivery metadata. scope is None for operation-level notifications that belong to no run.

    namespace is a reserved word in MoonBit, so the field is namespace_ and namespace_of() is the plain-reader accessor.

    Example

    test {
    let envelope = @events.EventEnvelope(
    namespace_=@events.Posoco,
    event=@events.Core(TurnStarted),
    )
    assert_eq(envelope.namespace_of(), @events.Posoco)
    assert_eq(envelope.scope, None)
    guard envelope.event is @events.Core(TurnStarted) else {
    fail("expected a core turn start")
    }
    }

    EventEnvelope::EventEnvelope

    fn EventEnvelope::EventEnvelope(namespace_~ : EventNamespace, event~ : Event, scope? :
    EventScope
    ?) -> EventEnvelope

    Framework construction hook: the runtime stamps real delivery metadata. Subscribers receive envelopes and do not need this constructor.

    EventEnvelope::namespace_of

    fn EventEnvelope::namespace_of(self : EventEnvelope) -> EventNamespace

    EventError

    pub(all) enum EventError {
    Closed
    ReservedEvent
    } derive(Eq,
    Debug
    )

    Typed failure of an Events operation. Operations never fail silently.

    EventError::equal

    fn EventError::equal(EventError, EventError) -> Bool

    EventError::not_equal

    fn EventError::not_equal(x : EventError, y : EventError) -> Bool

    EventNamespace

    pub(all) enum EventNamespace {
    Posoco
    Extension(String)
    } derive(Eq,
    Debug
    )

    Delivery namespace of one event. Posoco is reserved for core notifications; an extension capability publishes only its own Extension(manifest_id). The namespace is system-produced envelope metadata, never a string a publisher can spoof.

    EventNamespace::equal

    EventNamespace::not_equal

    fn EventNamespace::not_equal(x : EventNamespace, y : EventNamespace) -> Bool

    Events

    pub struct Events {
    // private fields
    }

    Owner-bound event capability. publish is fixed to the namespace the Agent granted — an extension's manifest id — while subscribe may listen to any namespace. Product code obtains the value from CompositionView::events; only Posoco core holds the core publish handle.

    Events::from_callbacks

    fn Events::from_callbacks(publish~ : (Event,
    EventScope
    ?) -> Result[Unit, EventError], subscribe~ : ((EventEnvelope) -> Unit, EventNamespace?) -> Result[Subscription, EventError]) -> Events

    Framework construction hook. Product code obtains the value through CompositionView::events and does not need this constructor.

    Events::publish

    fn Events::publish(self : Events, event : Event, scope? :
    EventScope
    ?) -> Result[Unit, EventError]

    Publish one event in the namespace this capability is bound to. scope carries session/run/turn attribution when the notification belongs to a run. An extension capability holding a core payload fails with ReservedEvent; a closed Agent fails with Closed; no listener succeeds with no side effect. A community payload is delivered as published, so it must be immutable data rather than a value the publisher mutates later.

    Example

    test {
    // This fake models the real Agent's reserved-payload rejection.
    // Extensions receive their capability from CompositionView, not this factory.
    let events = @events.Events::from_callbacks(
    publish=fn(event, _scope) {
    match event {
    @events.Core(_) => Err(@events.ReservedEvent)
    _ => Ok(())
    }
    },
    subscribe=fn(_handler, _namespace_) { Err(@events.Closed) },
    )
    guard events.publish(@events.Core(TurnStarted)) is Err(ReservedEvent) else {
    fail("extensions must not publish core notifications")
    }
    }

    Events::subscribe

    fn Events::subscribe(self : Events, handler : (EventEnvelope) -> Unit, namespace_? : EventNamespace?) -> Result[Subscription, EventError]

    Register one synchronous, non-raising listener. Delivery follows registration order; the namespace_ argument narrows delivery to one namespace, and None listens to every namespace including Posoco. A listener added or cancelled inside a delivery batch takes effect from the next batch, and a new listener receives no history.

    Example

    test {
    let seen_filter : Ref[@events.EventNamespace?] = Ref(None)
    let events = @events.Events::from_callbacks(
    publish=fn(_event, _scope) { Ok(()) },
    subscribe=fn(_handler, namespace_) {
    seen_filter.val = namespace_
    Ok(@events.Subscription::from_cancel(fn() { () }))
    },
    )
    let subscription = events.subscribe(
    fn(_envelope) { () },
    namespace_=Some(@events.Posoco),
    )
    guard subscription is Ok(_) else { fail("expected a subscription") }
    assert_eq(seen_filter.val, Some(@events.Posoco))
    }

    Subscription

    pub struct Subscription {
    // private fields
    }

    Opaque handle of one listener registration. cancel is idempotent and releases the handler reference; a handle whose runtime is gone cancels to a no-op.

    Example

    test {
    let cancels = Ref(0)
    let subscription = @events.Subscription::from_cancel(fn() {
    cancels.val = cancels.val + 1
    })
    subscription.cancel()
    subscription.cancel()
    assert_eq(cancels.val, 1)
    }

    Subscription::cancel

    fn Subscription::cancel(self : Subscription) -> Unit

    Subscription::from_cancel

    fn Subscription::from_cancel(cancel : () -> Unit) -> Subscription

    Framework construction hook. Product code receives subscriptions from Events::subscribe and does not need this constructor.

    Powered by MoonBit

    Site sourceReport issueModulesSkillsBuild queueStatistics

    © 2026 mooncakes.io