Browse API 1 documentation
Event reference
Event payloads, permissions, retention, and callback behavior.
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
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.
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.
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
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.
grove.on('channels', event => {
document.querySelector('#channel-count').textContent = String(event.channels.length)
})
channelSnapshots
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.
let currentChannels = []
grove.on('channelSnapshots', event => {
currentChannels = event.channels
})
command
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.
grove.on('command', event => {
if (event.command === 'lookup') {
document.querySelector('#search').value = event.argString
}
})
See grove.commands.register().
memberAction
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.
grove.on('memberAction', event => {
if (event.action === 'profile') {
document.querySelector('#nickname').textContent = event.member
}
})
See grove.memberActions.register().
channelAction
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.
grove.on('channelAction', event => {
if (event.action === 'inspect') {
document.querySelector('#channel').textContent = event.channel
}
})
See grove.channelActions.register().
lifecycle
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.
grove.on('lifecycle', ({ state }) => {
if (state === 'foreground' || state === 'pulse') {
// Check real timestamps before reconnecting a stale socket.
}
})
launch
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.
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
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.
grove.on('queryRenamed', async event => {
// Application-owned implementation must be idempotent and fail closed.
await updateProtocolIdentity(event.oldTargetKey, event.newTargetKey, event.outcome)
})