@amends-dev/channel
Channels bring external conversations in. A channel transport is how one external system, such as Slack, an email service, or a chat bridge, is received from and replied to: it verifies and reads what the system sends, as events in the system’s conversations, and posts replies there. What becomes of the events, which messages are posted, and who sees what are the application’s concerns; the transport knows its system alone. The package depends only on Effect.
Install
Section titled “Install”bun add @amends-dev/channel effectAdd a transport for each system you connect, written as an adapter. In
amends, @amends-dev/connection installs a transport as what an integration gives
its connections, and core’s channels take its events in as threads.
The contract
Section titled “The contract”ChannelTransport.ChannelTransport has four members and two optional ones:
| Member | What it does |
|---|---|
self |
The transport’s own identity in the system, such as its bot user, or undefined when it has none. What it posted and receives back is known as an echo by it, and by the ids post gave. |
receive |
Takes an Inbound request the system sent the host, its headers and body as they came, verifies it as the system signs it, and gives Received: the events in it, none for a request that carries no message, and the answer the system expects when it does, as a URL verification challenge. |
post |
Sends Outgoing, a text and its author’s name, as a reply in a Conversation, and gives Posted, the messageId the system assigned. |
edit |
Optional: replaces a posted message’s text, where the system allows it. |
delete |
Optional: removes a posted message, where the system allows it. |
An Event is one of MessageReceived, MessageEdited, or MessageDeleted. Each names the
conversation, the messageId, the sender, and when it happened; a received or edited message
carries its text, and a received one may carry a title for a conversation that has one, such as
an email’s subject. A Conversation is the system’s id for it, which must be stable, since it
keys everything that comes of the conversation, and details, whatever the transport needs to post
there later, kept as JSON and given back to post as it was: a Slack channel and thread timestamp,
an email’s message id to reply to. A Sender is the person’s id in the system and what they are
called. Every id is an ExternalId, a non-empty string the system owns.
Every operation fails with TransportError, whose reason says what to do: RejectedError, the
request is not the system’s or cannot be read, and the system should be answered so; RefusedError,
the system refused the operation for good, such as a message in an archived conversation; and
UnavailableError, the system could not be reached now, which isRetryable says, so the caller may
try again. Each reason’s description says what happened, and cause carries what the system said.
A caller recovers from one reason with Effect.catchReason("TransportError", "RejectedError", ...).
Writing a transport
Section titled “Writing a transport”A transport is a plain object, usually made once per connection to the system with the connection’s settings and secret. Slack, for example:
import { ChannelTransport } from "@amends-dev/channel"import { Effect, Schema } from "effect"
export const slack = (options: { readonly botToken: string readonly signingSecret: string readonly botUserId: string}): ChannelTransport.ChannelTransport => ({ self: ChannelTransport.ExternalId.make(options.botUserId), receive: (inbound) => Effect.gen(function* () { yield* verifySignature(inbound, options.signingSecret) // TransportError with a RejectedError const payload = yield* decodeEventsApi(inbound.body) if (payload.type === "url_verification") return { events: [], answer: payload.challenge } return { events: yield* eventsOf(payload) } }), post: (conversation, outgoing) => chatPostMessage(options.botToken, { channel: conversation.details.channel, thread_ts: conversation.details.thread_ts, text: `${outgoing.author}: ${outgoing.text}` }).pipe(Effect.map((posted) => ({ messageId: posted.ts })))})A MessageReceived event’s conversation is the Slack channel and thread it arrived in, with the
message’s ts as its id, and the sender resolved from the user id by one lookup. Edits and
deletions arrive as message_changed and message_deleted subtypes, and become MessageEdited
and MessageDeleted. A system that only pushes to a public URL, such as Slack or Google’s
notifications for Gmail, is served by a host that exposes its events route; a system with no API,
such as iMessage, is served by a bridge that posts to that route and that post calls in turn.
Keep the transport stateless where you can: it is made for each request. Reading the same message twice is safe, since what consumes the events derives every id from the system’s and treats a repeat as nothing new.
Development
Section titled “Development”The package is a contract and has no tests of its own: @amends-dev/connection checks what it does
with a transport, and the gateway’s tests run channels on a test transport.
bun run dist check packages/channel builds the package as npm
gets it and checks it as a consumer.