Browse API 1 documentation
GroveAPI 1preview

Event reference

Event payloads, permissions, retention, and callback behavior.

Mango 3.1+View Markdown

Subscribe with grove.on() and remove listeners with grove.off(). Names are case-sensitive. Register during initial script execution: only the three retained events below replay for a late JavaScript subscriber. Native queues buffer some events while a document loads, but they are bounded and do not provide a durable event log.

Event index

Name Payload Useful delivery / permission Retained
message GroveMessage channel.read for bound/joined channels; query.read for private messages No
messageEcho GroveMessage Same read gates as message No
channels ChannelRoster Server scope and channel.read Latest
channelSnapshots ChannelSnapshots Channel/server scope and channel.members and/or channel.metadata Latest
command CommandInvocation Registered/manifest-declared command and commands.register No
memberAction MemberActionInvocation Server scope and ui.member-actions No
channelAction ChannelActionInvocation Server scope and ui.channel-actions No
lifecycle LifecycleEvent Mounted runtime; no extra capability No
launch LaunchEvent Launch from a grove://run link; no extra capability Latest
queryRenamed QueryRenameEvent Server scope and messages.protect No; host queues/retries

The facade may deliver empty channels/channelSnapshots arrays when no data is eligible. Such an empty event does not indicate permission or server-wide read access.

message

ts
interface GroveMessage {
  kind: 'channel' | 'dm'
  from: string
  own: boolean
  text: string
  channel?: string
  id?: string
  account?: string
  host?: string
  receipt?: string
  receiptCorrelation?: 'labeledResponse' | 'bestEffort' | 'transportAccepted'
}
Field Meaning
kind channel for a channel message, dm for a private conversation.
from Incoming sender nickname. On own DM echoes, this is the counterpart nickname so the query can be identified.
own Whether this is output from the current IRC identity. Present as a boolean in the current implementation.
text Delivered message body. Do not treat it as safe HTML or as a filter verdict.
channel Channel name for channel messages; omitted for DMs.
id IRC message ID when available; absent on some messages and transport-acceptance fallbacks.
account Optional services account supplied by the network.
host Optional user@host identity supplied by the network.
receipt Optional local UUID from the originating instance’s corresponding send.
receiptCorrelation Strength of local send correlation when a receipt is present; see messageEcho below.

Channel delivery is limited to the bound channel or, in server scope, confirmed joined channels. It is live delivery, not a CHATHISTORY API. query.read can deliver network private messages to an eligible IRC-bound runtime; the current routing selects at most one instance of each extension per network to avoid duplicate bot replies. Do not assume it is an inbox restricted to the query where the extension was launched.

Own channel messages also arrive on message for compatibility. Own DMs arrive only on messageEcho. Use own to avoid auto-reply loops. Nicknames and hostmasks are not durable authentication; account is useful when supplied, but every payload remains untrusted input.

js
grove.on('message', message => {
  if (message.own) return
  document.querySelector('#latest').textContent = message.text
})

messageEcho

Uses GroveMessage above, with own true. Requires the matching channel.read or query.read permission even if send authority was granted.

With IRCv3 echo-message, the event follows an own server echo, which can include output from other clients on the same bouncer. Without echo-message, or for local bouncer-service sends that bypass echo tracking, a receipt event follows native transport acceptance of the complete send. Own channel output is also delivered on message; own DM output only on messageEcho.

A supplied receipt is exposed only to its originating live instance. It is not sent as an IRC tag. No failure event or timeout is provided; applications needing durable work must manage persistence and bounded retries themselves. The UUID identifies local pending work; it does not turn IRC echoes into an end-to-end delivery protocol.

receiptCorrelation Meaning
labeledResponse A separate host-generated IRC label correlated the echo to this send. Server text rewriting does not prevent the match.
bestEffort No label was used; Mango matched target and exact wire text, oldest first. Identical output from another client may be ambiguous and server rewriting may prevent a match. History reconciliation is also best-effort.
transportAccepted Native transport accepted the send; no server acknowledgement.

Absent correlation on older hosts means unknown strength. Unlabeled echoes do not consume pending labeled sends. A receipt on a split send identifies its last logical message, not an atomic acknowledgement of every fragment. Missing receipts mean unknown outcomes; retries may duplicate output. No correlation value proves recipient delivery or reading.

js
const pending = new Set()
grove.on('messageEcho', message => {
  if (message.receipt && pending.delete(message.receipt)) {
    console.info('Pending send acknowledged')
  }
})
// In a bound channel with channel.send and channel.read:
const receipt = crypto.randomUUID()
pending.add(receipt)
grove.send('Hello', { receipt })

See grove.send().

channels

ts
interface ChannelRoster {
  channels: string[]
  caseMapping: 'ascii' | 'rfc1459' | 'strict-rfc1459'
}

channels is the complete list of confirmed joined channel names for a server-scoped reader. Pending joins and parted channels are excluded. caseMapping describes the server’s negotiated identifier comparison. JavaScript toLowerCase is not IRC RFC 1459 folding. Prefer host-provided canonical keys when another event supplies them.

The latest snapshot is retained and immediately delivered to a late subscriber. This is membership context, not a grant to send or read member rosters.

js
grove.on('channels', event => {
  document.querySelector('#channel-count').textContent = String(event.channels.length)
})

channelSnapshots

ts
interface ChannelSnapshots {
  channels: {
    channel: string
    channelKey: string
    topic?: string
    modes: string[]
    members?: {
      nickname: string
      memberKey: string
      modes: string[]
      prefixes: string
      away: boolean
    }[]
  }[]
}
Field Meaning
channel / channelKey Display channel name and exact host-canonical lookup key.
topic Present when available and channel.metadata is granted; otherwise omitted.
modes Safe boolean channel-mode letters without +. Always an array; empty when metadata is not granted. Sensitive argument values such as channel keys are never exposed.
members Included only with channel.members. Missing is distinct from an empty roster.
nickname / memberKey Member display nickname and host-canonical key.
member modes Every retained membership mode, e.g. [‘o’, ‘v’]; not just the highest mode.
prefixes Corresponding display prefixes, e.g. ‘@+’.
away Host’s current boolean away status for the member.

Channel scope receives only its confirmed bound channel; server scope receives confirmed joined channels. Query/application scopes have no eligible channel snapshots. Either read grant can be used alone; metadata is not granted through member permission or a UI badge permission. The latest complete payload is retained for late listeners. Replace prior local snapshot state rather than accumulating entries indefinitely.

js
let currentChannels = []
grove.on('channelSnapshots', event => {
  currentChannels = event.channels
})

command

ts
interface CommandInvocation {
  command: string
  args: string[]
  argString: string
  target?: string
  targetKey?: string
}

command is the normalized name without slash. argString is the trimmed raw argument string; args splits that string on whitespace without shell quoting or escaping. Optional target is the invoking buffer name and targetKey its host-canonical key; both are omitted when no channel/query context exists.

Subcommands remain part of args/argString. A card button routes through this same event. Composer commands declared in the manifest launch the utility and deliver the event even though that utility cannot register commands at runtime. Application Roots receive no command stream.

At most 50 command invocations are retained in the native per-instance queue. This does not replay an event for a listener added after its delivery to JavaScript.

js
grove.on('command', event => {
  if (event.command === 'lookup') {
    document.querySelector('#search').value = event.argString
  }
})

See grove.commands.register().

memberAction

ts
interface MemberActionInvocation {
  action: string
  networkID: string
  channel: string
  channelKey: string
  member: string
  memberKey: string
}

action is the normalized registered action id. networkID is the host’s network context; channel/member preserve display names, while channelKey/memberKey are canonical lookup keys. The event follows an explicit native menu selection, not arbitrary pointer activity. No message contents or roster access follows from this grant. Up to 50 native invocations are queued per instance.

js
grove.on('memberAction', event => {
  if (event.action === 'profile') {
    document.querySelector('#nickname').textContent = event.member
  }
})

See grove.memberActions.register().

channelAction

ts
interface ChannelActionInvocation {
  action: string
  networkID: string
  channel: string
  channelKey: string
}

action is the registered action id. The remaining fields identify the explicit user-selected joined channel and its network. channelKey is canonical. Up to 50 native invocations are queued per instance. The UI capability itself supplies no send authority.

js
grove.on('channelAction', event => {
  if (event.action === 'inspect') {
    document.querySelector('#channel').textContent = event.channel
  }
})

See grove.channelActions.register().

lifecycle

ts
interface LifecycleEvent {
  state: 'foreground' | 'background' | 'pulse'
}

foreground/background report application lifecycle transitions to mounted documents. pulse is a periodic native wake-up for runtimes with WSS egress (currently about every 20 seconds while the host is able to run). It helps recheck actual heartbeat deadlines; it is not a guaranteed scheduler. Settings pages have no lifecycle stream.

There is no guaranteed final shutdown callback. Persist important state as it changes and tolerate suspension, process loss, and reload. Ordinary lifecycle callbacks are not awaited.

js
grove.on('lifecycle', ({ state }) => {
  if (state === 'foreground' || state === 'pulse') {
    // Check real timestamps before reconnecting a stale socket.
  }
})

launch

ts
interface LaunchEvent {
  url: string
  extensionID: string
  args: string[]
  parameters: { name: string; value: string | null }[]
}

Emitted when an installed extension starts from grove://run/<extension-id>/<argument>/… . url is the original launch URL, extensionID the selected package, args independently percent-decoded path components, and parameters ordered query entries. Duplicate names are preserved; a valueless flag has null value; an absent query produces [].

The latest launch is retained for a late listener and delivered again after WebContent recovery. Treat it as state restoration, not permission to repeat a transaction. Ordinary manager/composer launches produce no launch event. No extra capability is needed, and the link never installs code or grants network access.

js
grove.on('launch', event => {
  const pollID = event.args[0]
  if (typeof pollID !== 'string' || !/^[A-Za-z0-9_-]{1,80}$/.test(pollID)) return
  document.querySelector('#poll-id').textContent = pollID
})

See Launch links for URL construction, size limits, consent, and cold-start behavior.

queryRenamed

ts
interface QueryRenameEvent {
  oldTarget: string
  oldTargetKey: string
  newTarget: string
  newTargetKey: string
  outcome: 'renamed' | 'collision'
}

Delivered to server-scoped protected-message runtimes after an IRC nickname change affects an open query. Display names are kept separately from canonical keys. renamed means Mango moved the query/policy and reset protection status to negotiating. collision means the destination query was already open and Mango left both buffers in place.

Mango awaits each callback in registration order. A rejected callback prevents advancement of the native cursor and triggers bounded retries; undelivered queued events can retry after WebContent reload. Make updates idempotent: earlier callbacks may run again after a later callback fails. Native queues are bounded (200 entries), so this is not an unlimited durable history. Register before asynchronous initialization and serialize your state access.

Keep both identities and require user resolution when a collision is found, including a collision in separately persisted extension state. Do not overwrite keys merely because a nickname moved.

js
grove.on('queryRenamed', async event => {
  // Application-owned implementation must be idempotent and fail closed.
  await updateProtocolIdentity(event.oldTargetKey, event.newTargetKey, event.outcome)
})

See grove.protection.set().