diff --git a/blog/2026-05-19-new-in-v1.13.mdx b/blog/2026-05-19-new-in-v1.13.mdx new file mode 100644 index 0000000..4cfad72 --- /dev/null +++ b/blog/2026-05-19-new-in-v1.13.mdx @@ -0,0 +1,136 @@ +--- +slug: message-reactions-and-presence-pubsub +title: Message reactions, presence pub/sub, and heartbeat enforcement +authors: [james] +tags: [releases, features, channels, presence] +description: Hotsock v1.13 adds message reactions with per-event permissions and real-time delivery, presence member events to SNS/EventBridge, and a heartbeatTimeout claim for server-enforced connection liveness. +--- + +Hotsock v1.13 introduces **message reactions**, **presence member events on pub/sub (SNS/EventBridge)**, and a **`heartbeatTimeout` claim** for server-enforced connection liveness. + +{/* truncate */} + +### Message reactions {/* #message-reactions */} + +Clients can now add and remove reactions on stored messages, with real-time delivery to all channel subscribers, aggregated counts in message history, and a separate endpoint to list who reacted with what. + +Reactions are gated by a new [`react`](/docs/connections/claims/#channels.messages.react) directive on channel message event patterns, alongside the existing [`publish`](/docs/connections/claims/#channels.messages.publish), [`store`](/docs/connections/claims/#channels.messages.store), [`echo`](/docs/connections/claims/#channels.messages.echo), and other directives. A connection needs three things to react: an active channel subscription, a [`uid`](/docs/connections/claims/#uid), and a stored target message that hasn't expired. + +#### Setting up permissions {/* #setting-up-permissions */} + +The `react` directive accepts `true` (allow any reaction), `false` (deny), or an array of strings to restrict which reactions are permitted: + +```json +{ + "exp": 1747574400, + "scope": "connect", + "uid": "Jim", + "channels": { + "room.123": { + "subscribe": true, + "messages": { + "chat": { + "publish": true, + "echo": true, + "store": 31536000, + // highlight-next-line + "react": ["๐", "โค๏ธ", "๐"] + } + } + } + } +} +``` + +#### Adding and removing reactions {/* #adding-and-removing-reactions */} + +Clients send a `hotsock.messageReaction` message on the WebSocket with the target message ID, the reaction value, and an explicit `add` or `remove` action: + +``` +> {"event":"hotsock.messageReaction", "channel":"room.123", "data":{"messageId":"01JA3S0TNEC2QBYMZRBMCEXGNW", "reaction":"๐", "action":"add"}} +``` + +All channel subscribers immediately receive a `hotsock.messageReactionAdded` event: + +``` +< {"id":"01JB4K7M2N...","event":"hotsock.messageReactionAdded","channel":"room.123","data":{"messageId":"01JA3S0TNEC2QBYMZRBMCEXGNW","reaction":"๐","action":"add"},"meta":{"uid":"Jim","umd":null}} +``` + +Reaction data can be emoji characters, Slack-style colon-wrapped strings like `:thumbsup:`, or any short string up to 100 bytes. Each user can have one reaction per reaction value per message. + +#### Reactions in message history {/* #reactions-in-message-history */} + +[`listMessages`](/docs/connections/client-http-api/#connection/listMessages) responses now include a `reactions` object on each message that has reactions, keyed by reaction value: + +```json +{ + "messages": [ + { + "id": "01JA3S0P7FB7WVYS67X316M32S", + "event": "chat", + "channel": "room.123", + "data": "Wow. Can we make it a different moment?", + "meta": { "uid": "Jim", "umd": null }, + // highlight-next-line + "reactions": { "๐": { "count": 2 }, "โค๏ธ": { "count": 1 } } + } + ] +} +``` + +Pass [`expandReactions: true`](/docs/connections/client-http-api/#connection/listMessages.expandReactions) to also include an `items` array per reaction with the user behind each one: + +```json +"reactions": { + "๐": { + "count": 2, + "items": [ + { "uid": "Jim", "umd": { "name": "Jim Halpert" } }, + { "uid": "Pam" } + ] + } +} +``` + +You can also fetch reactions for a single message with the dedicated [`connection/listReactions`](/docs/connections/client-http-api/#connection/listReactions) endpoint. + +#### Pub/sub events for reactions {/* #pubsub-events-for-reactions */} + +[`hotsock.messageReactionAdded`](/docs/server-api/events/#hotsock.messageReactionAdded) and [`hotsock.messageReactionRemoved`](/docs/server-api/events/#hotsock.messageReactionRemoved) events are emitted to SNS/EventBridge after reactions are persisted, gated by the [`emitPubSubEvent`](/docs/connections/claims/#channels.messages.emitPubSubEvent) directive on the matching message event pattern. + +### Presence member events on pub/sub (SNS/EventBridge) {/* #presence-member-pubsub */} + +Presence channel member lifecycle events ([`hotsock.memberAdded`](/docs/server-api/events/#hotsock.memberAdded), [`hotsock.memberRemoved`](/docs/server-api/events/#hotsock.memberRemoved), [`hotsock.memberUpdated`](/docs/server-api/events/#hotsock.memberUpdated)) are now also emitted to SNS/EventBridge, gated by the existing [`PublishEventsToSNSParameter` / `PublishEventsToEventBridgeParameter`](/docs/server-api/events/#eventbridge-sns-enable-disable) settings. The pub/sub events are deduplicated by [`uid`](/docs/connections/claims/#uid) to match WebSocket fan-out semantics, so joining from a second device when you're already present doesn't trigger another `memberAdded`. + +The pub/sub `data` payload uses the same single-entity shape as the rest of the public event types, with [`uid`](/docs/connections/claims/#uid), [`umd`](/docs/connections/claims/#umd), and `members` at the root: + +```json +{ + "uid": "Dwight", + "umd": { "status": "available" }, + "members": [ + { "uid": "Jim", "umd": null }, + { "uid": "Dwight", "umd": { "status": "available" } } + ] +} +``` + +`dataType` is `channelMember`, so SNS filter policies and EventBridge rules can target presence events specifically. + +### Server-enforced heartbeats {/* #server-enforced-heartbeats */} + +The new [`heartbeatTimeout`](/docs/connections/claims/#heartbeatTimeout) connect-token claim sets a maximum interval between client-initiated [`hotsock.heartbeat`](/docs/connections/keep-alive/#send-hotsockheartbeat) messages. If the server doesn't see a heartbeat within that window, the connection is forcefully disconnected: + +```json +{ + "scope": "connect", + // highlight-next-line + "heartbeatTimeout": 30 +} +``` + +Valid values are 5 to 600 seconds. This is most useful on [presence channels](/docs/channels/presence/) where accurate detection of dropped clients matters more than waiting for API Gateway's 10-minute idle timeout. The client just needs to send `{"event":"hotsock.heartbeat"}` periodically, and the server records receipt and reschedules the next check relative to the last heartbeat, with no reply. + +### Wrapping up {/* #wrapping-up */} + +Existing installations with auto-update enabled are already running v1.13 and have access to these features today. Other installations can be [manually updated](/docs/installation/updates/#manually-update-installation) at any time. A [full changelog](/docs/installation/changelog/#v1.13.0) is available with the complete list of changes included in this release. diff --git a/docs/connections/claims.mdx b/docs/connections/claims.mdx index b9c791c..eed0923 100644 --- a/docs/connections/claims.mdx +++ b/docs/connections/claims.mdx @@ -226,7 +226,7 @@ If you wanted to allow access to the entire message history of a channel without `Object` (optional) - Manages the permissions and directives for client-initiated messages that are published directly to the WebSocket on the channel(s) for the specified events. Each object key is the name of an event and can include asterisks (\*) anywhere in the string to denote wildcards. Keys can also use the `#regex:` prefix to specify a regular expression pattern for matching event names. Each object value is another object with the settings for that event or event pattern. -Each object inside the messages object accepts [`broadcast`](#channels.messages.broadcast), [`echo`](#channels.messages.echo), [`emitPubSubEvent`](#channels.messages.emitPubSubEvent), [`publish`](#channels.messages.publish), [`scheduleBefore`](#channels.messages.scheduleBefore), and [`store`](#channels.messages.store) attributes. +Each object inside the messages object accepts [`broadcast`](#channels.messages.broadcast), [`echo`](#channels.messages.echo), [`emitPubSubEvent`](#channels.messages.emitPubSubEvent), [`publish`](#channels.messages.publish), [`react`](#channels.messages.react), [`scheduleBefore`](#channels.messages.scheduleBefore), and [`store`](#channels.messages.store) attributes. #### Regex patterns {/* #channels.messages--regex */} @@ -405,6 +405,81 @@ In the following example, since `chat.*` includes `chat.admin`, this connection ::: +#### `react` {/* #channels.messages.react */} + +`Boolean` or `Array[String]` (optional) - Controls whether this connection can add or remove reactions on stored messages that match this event pattern. Reactions require an active channel subscription and a [`uid`](#uid) on the connection. Default is `false`. + +When set to `true`, any reaction value is permitted. When set to an array of strings, only reaction values in the list are allowed. An explicit `false` denies reactions. + +When a reaction is added or removed, all channel subscribers receive a `hotsock.messageReactionAdded` or `hotsock.messageReactionRemoved` message in real-time. Reaction counts are included in [`listMessages`](./client-http-api.mdx#connection/listMessages) responses, and individual reactions can be listed with [`listReactions`](./client-http-api.mdx#connection/listReactions). + +The following allows any reaction on `chat` messages in the "mychannel" channel: + +```json +{ + "channels": { + "mychannel": { + "subscribe": true, + "messages": { + "chat": { + "publish": true, + "echo": true, + "store": 31536000, + // highlight-next-line + "react": true + } + } + } + } +} +``` + +To restrict to specific reactions, pass an array: + +```json +{ + "channels": { + "mychannel": { + "subscribe": true, + "messages": { + "chat": { + "publish": true, + "store": 31536000, + // highlight-next-line + "react": ["thumbsup", "heart", "laugh"] + } + } + } + } +} +``` + +To add a reaction, send a `hotsock.messageReaction` message on the WebSocket: + +``` +> {"event":"hotsock.messageReaction", "channel":"mychannel", "data":{"messageId":"01JA3S0TNEC2QBYMZRBMCEXGNW", "reaction":"thumbsup", "action":"add"}} +``` + +To remove a reaction: + +``` +> {"event":"hotsock.messageReaction", "channel":"mychannel", "data":{"messageId":"01JA3S0TNEC2QBYMZRBMCEXGNW", "reaction":"thumbsup", "action":"remove"}} +``` + +All channel subscribers receive the reaction event: + +``` +< {"id":"01JB4K7M2N...","event":"hotsock.messageReactionAdded","channel":"mychannel","data":{"messageId":"01JA3S0TNEC2QBYMZRBMCEXGNW","reaction":"thumbsup","action":"add"},"meta":{"uid":"Jim","umd":null}} +``` + +Reaction data must not be blank, must be no more than 100 bytes, and must not contain number signs (`#`). Reactions can be emoji characters, Slack-style colon-wrapped strings like `:thumbsup:`, or any other short string. + +:::info +Reactions can only be added to stored messages (messages published with a [`store`](#channels.messages.store) TTL). The target message must exist and not be expired. Each user can have one reaction per reaction value per message. +::: + +Also available via the Client HTTP API at [`connection/publishMessage`](./client-http-api.mdx#connection/publishMessage) using `"event": "hotsock.messageReaction"` with the reaction payload as `data`, or via the server-side [Lambda](../server-api/publish-messages.mdx#publish-with-lambda) and [HTTP URL](../server-api/publish-messages.mdx#publish-with-http-url) publish APIs. + #### `scheduleBefore` {/* #channels.messages.scheduleBefore */} `NumericDate` (optional) - If [client-initiated message publishing](#channels.messages.publish) is permitted for this channel and event, the `scheduleBefore` attribute specifies the furthest out future time that this connection can schedule messages for delivery. If supplied, the schedule before time must be expressed as a Unix timestamp - the number of seconds since the Unix epoch. By default, or if `scheduleBefore` is provided as `0` or any timestamp in the past, scheduled message publishing is not permitted. @@ -747,6 +822,20 @@ You can also explicitly set `subscribe` to `false`, preventing subscriptions to } ``` +## `heartbeatTimeout` {/* #heartbeatTimeout */} + +`Integer` (optional) - The maximum number of seconds between client-initiated heartbeats. If set, the connection must send a [`hotsock.heartbeat`](./keep-alive.mdx#send-hotsockheartbeat) event at least this often or it will be forcefully disconnected by the server. Must be between `5` and `600` (inclusive) if provided. Default behavior (when unset) is no heartbeat enforcement. + +This is useful for [presence channels](../channels/presence.mdx) and other use cases where accurate detection of dropped clients matters more than the default API Gateway idle timeout would provide. + +```json +{ + "scope": "connect", + // highlight-next-line + "heartbeatTimeout": 30 +} +``` + ## `iat` - Issued At {/* #iat */} `NumericDate` (optional) - The issued at claim identifies the time when the JWT was issued, expressed as a Unix timestamp. Hotsock does not rely on this claim for authorization, but it's common for issuers to provide it by default which is why it's mentioned here. diff --git a/docs/connections/client-http-api.mdx b/docs/connections/client-http-api.mdx index 33ee8de..ff3df6a 100644 --- a/docs/connections/client-http-api.mdx +++ b/docs/connections/client-http-api.mdx @@ -5,7 +5,7 @@ toc_max_heading_level: 4 # Client HTTP API -In addition to WebSocket interactions, connected clients can also make use of an HTTP API for publishing messages, listing message history, reading and writing channel storage, and updating user metadata. All requests **_must_ be `POST` requests** and **_must_ have URL-encoded `connectionId` and `connectionSecret`** query parameters set. +In addition to WebSocket interactions, connected clients can also make use of an HTTP API for publishing messages, listing message history, listing reactions, reading and writing channel storage, and updating user metadata. All requests **_must_ be `POST` requests** and **_must_ have URL-encoded `connectionId` and `connectionSecret`** query parameters set. ## Determine your API URL {/* #determine-your-api-url */} @@ -59,6 +59,8 @@ When issuing a connect or subscribe token, specify the [`historyStart`](./claims This endpoint will return up to 100 messages or up to 1MB of data, whichever comes first. Fetch subsequent pages using `before` or `after`, depending on your use case. +If any returned messages have reactions, a `reactions` object is included on each message, keyed by reaction value with `{count, items?}` per entry. By default only `count` is set. Pass [`expandReactions: true`](#connection/listMessages.expandReactions) to also populate `items` with one entry per reacting user. + #### `after` {/* #connection/listMessages.after */} String (optional) - Load messages after (newer than) the message with the ID specified here. Has no effect if `reverse` is `true`. @@ -79,6 +81,10 @@ Example: `01JA3HVNF6E89HDT1ABRFWJ8C8` String (required) - The name of the channel to query message history. +#### `expandReactions` {/* #connection/listMessages.expandReactions */} + +Boolean (optional) - If `true`, the `reactions` object on each returned message is enriched with an `items` array per reaction containing the `uid` and `umd` of each reacting user. Default is `false`, which returns aggregated counts only. Messages with no reactions omit the field when `false` and emit `{}` when `true`. + #### `reverse` {/* #connection/listMessages.reverse */} Boolean (optional) - If set to `true`, lists messages from newest to oldest. Default is `false`. @@ -122,7 +128,9 @@ fetch( "meta": { "uid": "Jim", "umd": null - } + }, + // highlight-next-line + "reactions": { "thumbsup": { "count": 2 }, "heart": { "count": 1 } } }, { "id": "01JA3S0GB6S3WNTV2S1RJ421TH", @@ -152,7 +160,93 @@ fetch( "meta": { "uid": "Pam", "umd": null + }, + // highlight-next-line + "reactions": { "heart": { "count": 3 } } + } + ] +} +``` + +With `expandReactions: true`, each entry also has an `items` array with one element per reacting user: + +```json +{ + "messages": [ + { + "id": "01JA3S0P7FB7WVYS67X316M32S", + "event": "my-event", + "channel": "my-channel", + "data": "Wow. Can we make it a different moment?", + "meta": { "uid": "Jim", "umd": null }, + // highlight-start + "reactions": { + "thumbsup": { + "count": 2, + "items": [ + { "uid": "Jim", "umd": { "name": "Jim Halpert" } }, + { "uid": "Pam" } + ] + }, + "heart": { + "count": 1, + "items": [{ "uid": "Dwight" }] + } } + // highlight-end + } + ] +} +``` + +## `connection/listReactions` {/* #connection/listReactions */} + +List the individual reactions on a specific stored message, including who reacted. The connection must be subscribed to the channel. + +#### `channel` {/* #connection/listReactions.channel */} + +`String` (required) - The name of the channel where the message was published. + +#### `messageId` {/* #connection/listReactions.messageId */} + +`String` (required) - The ID of the message to list reactions for. + +### Example {/* #connection/listReactions--example */} + +#### Request {/* #connection/listReactions--example-request */} + +```javascript +fetch( + "https://r6zcm2.lambda-url.us-east-1.on.aws/connection/listReactions?connectionId=fjlb_eHLIAMCKRg%3d&connectionSecret=SZy32Etv0KIbe4Jod6KH", + { + method: "POST", + body: JSON.stringify({ + channel: "my-channel", + messageId: "01JA3S0P7FB7WVYS67X316M32S", + }), + }, +) +``` + +#### Response {/* #connection/listReactions--example-response */} + +Expect a `200 OK` status code for successful reads. + +```json +{ + "reactions": [ + { + "reaction": "thumbsup", + "uid": "Jim", + "umd": { "name": "Jim Halpert" } + }, + { + "reaction": "thumbsup", + "uid": "Pam" + }, + { + "reaction": "heart", + "uid": "Dwight" } ] } diff --git a/docs/connections/keep-alive.mdx b/docs/connections/keep-alive.mdx index b2695f3..d6313fe 100644 --- a/docs/connections/keep-alive.mdx +++ b/docs/connections/keep-alive.mdx @@ -37,24 +37,19 @@ wscat -c 0.20s user 0.07s system 0% cpu 2:00:00.46 total Clients should not reply to server-initiated `hotsock.keepAlive` events. -{/* ## Send `hotsock.heartbeat` +## Send `hotsock.heartbeat` {/* #send-hotsockheartbeat */} Like other TCP-based protocols, WebSockets will happily assume that connections are still alive until proven otherwise or until they are explicitly closed. There are many circumstances, however, where the connection is dead but one side doesn't know it yet. Unexpected network disconnects and computers going to sleep are some examples of where this can happen. -Both [standard](../channels/standard) and [presence](../channels/presence) channels support optional [connection heartbeats](./claims.mdx). +This is particularly useful for [presence channels](../channels/presence.mdx) when accuracy of connected members is critical and your application cannot withstand a delay in detecting dropped connections. -This is particularly useful for [presence channels](../channels/presence) when accuracy of connected members is critical and your application cannot withstand a delay in detecting dropped connections. - -If enabled, the client _must_ send a `hotsock.heartbeat` event on the WebSocket at least as often as specified in the `clientHeartbeatInterval` claim. - -The backend records receipt of this message, but does not send a reply. Avoid over-sending heartbeats, as each heartbeat incurs a database write and a billable API Gateway message. +When the [`heartbeatTimeout` claim](./claims.mdx#heartbeatTimeout) is set, the client _must_ send a `hotsock.heartbeat` event on the WebSocket at least every `heartbeatTimeout` seconds. The server records receipt of this message but does not send a reply. Avoid over-sending heartbeats โ each heartbeat incurs a database write and a billable API Gateway message. ``` > {"event":"hotsock.heartbeat"} -> ``` -If the server does not receive a heartbeat message from the client within the timeframe specified by the `clientHeartbeatInterval` claim, that connection is forcefully closed by the server. */} +If the server does not receive a heartbeat message within the configured timeout, the connection is forcefully closed by the server. ## `hotsock.ping` and `hotsock.pong` {/* #hotsockping-and-hotsockpong */} diff --git a/docs/installation/changelog.mdx b/docs/installation/changelog.mdx index b648550..cb1261e 100644 --- a/docs/installation/changelog.mdx +++ b/docs/installation/changelog.mdx @@ -1,5 +1,18 @@ # Changelog +## v1.13.0 - May 19, 2026 {/* #v1.13.0 */} + +- Add [message reactions](../connections/claims.mdx#channels.messages.react). Clients send `hotsock.messageReaction` events with `add` or `remove` actions, gated by a new [`react`](../connections/claims.mdx#channels.messages.react) directive on channel message event patterns. The `react` directive accepts `true` (allow all reactions), `false` (deny), or an array of strings (allow only specific reactions). Reactions require an active channel subscription, a [`uid`](../connections/claims.mdx#uid), and a stored target message that hasn't expired. Also available via the [Client HTTP API](../connections/client-http-api.mdx) and the server-side [Lambda](../server-api/publish-messages.mdx#publish-with-lambda) and [HTTP URL](../server-api/publish-messages.mdx#publish-with-http-url) publish APIs. +- Reaction counts are automatically included in [`listMessages`](../connections/client-http-api.mdx#connection/listMessages) responses on each message that has reactions. Pass [`expandReactions: true`](../connections/client-http-api.mdx#connection/listMessages.expandReactions) to also receive a list of reacting users per reaction value. +- Add [`connection/listReactions`](../connections/client-http-api.mdx#connection/listReactions) Client HTTP API endpoint to list individual reactions on a single message with user attribution. +- Add [`hotsock.messageReactionAdded`](../server-api/events.mdx#hotsock.messageReactionAdded) and [`hotsock.messageReactionRemoved`](../server-api/events.mdx#hotsock.messageReactionRemoved) pub/sub events to SNS/EventBridge when reactions are added or removed. Gated by the [`emitPubSubEvent`](../connections/claims.mdx#channels.messages.emitPubSubEvent) directive. TTL-driven reaction deletes do not emit `messageReactionRemoved`. +- Publish [`hotsock.memberAdded`](../server-api/events.mdx#hotsock.memberAdded), [`hotsock.memberRemoved`](../server-api/events.mdx#hotsock.memberRemoved), and [`hotsock.memberUpdated`](../server-api/events.mdx#hotsock.memberUpdated) presence channel events to SNS/EventBridge, deduplicated by `uid` to match WebSocket fan-out semantics. +- Add [`heartbeatTimeout`](../connections/claims.mdx#heartbeatTimeout) connect-token claim for server-enforced connection heartbeats. When set (5โ600 seconds), the client must send a [`hotsock.heartbeat`](../connections/keep-alive.mdx#send-hotsockheartbeat) message at least that often or the server will forcefully disconnect. Useful for [presence channels](../channels/presence.mdx) and other applications that need accurate dropped-connection detection beyond what API Gateway's idle timeout provides. +- Improve subscription cleanup throughput when many connections disconnect or unsubscribe at the same time. +- Update the [Web Console](../server-api/web-console.mdx) with support for sending message reactions. +- Build with Go 1.26.3. +- Update all aws-sdk-go-v2 SDK modules to their latest versions (as of [2026-05-12](https://github.com/aws/aws-sdk-go-v2/releases/tag/release-2026-05-12)). + ## v1.12.0 - April 8, 2026 {/* #v1.12.0 */} - Add [channel storage](../connections/claims.mdx#channels.storage), a per-key persistent key-value store on channels. Storage permissions are configured per-key with wildcard and regex pattern support. Entries have independent TTLs, are delivered to [`observe`](../connections/claims.mdx#channels.storage.observe) subscribers on join, and skip fan-out when the value hasn't changed. Clients interact with storage via [`hotsock.channelStorageSet`](../connections/claims.mdx#channels.storage.set) / [`hotsock.channelStorageGet`](../connections/claims.mdx#channels.storage.get) on the WebSocket or the [`connection/channelStorageGet`](../connections/client-http-api.mdx#connection/channelStorageGet), [`connection/channelStorageSet`](../connections/client-http-api.mdx#connection/channelStorageSet), and [`connection/channelStorageList`](../connections/client-http-api.mdx#connection/channelStorageList) Client HTTP API endpoints. Storage operations do not require an active channel subscription. Server-side writes are available via the Lambda and HTTP publish APIs using `"event": "hotsock.channelStorageSet"` with a `key` field. diff --git a/docs/server-api/events.mdx b/docs/server-api/events.mdx index d60ec03..eefdc4e 100644 --- a/docs/server-api/events.mdx +++ b/docs/server-api/events.mdx @@ -17,7 +17,7 @@ Published events use the same shape for both EventBridge and SNS, where the payl "source": "hotsock.v1", "type": "hotsock.connected", "metadata": { - "hotsockVersion": "1.9.1" + "hotsockVersion": "1.13.0" }, "data": { "id": "IDnAdd9kIAMCEsQ=", @@ -25,8 +25,8 @@ Published events use the same shape for both EventBridge and SNS, where the payl "disconnectedAt": null, "keepAlive": true, "sourceIp": "12.34.56.78", - "uid": null, - "umd": null, + "uid": "12345", + "umd": { "firstName": "Jim", "lastName": "Halpert" }, "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0" }, "dataType": "connection" @@ -95,7 +95,7 @@ If enabled, all events are published to a Hotsock-specific event bus in your acc "source": "hotsock.v1", "type": "hotsock.connected", "metadata": { - "hotsockVersion": "1.9.1" + "hotsockVersion": "1.13.0" }, "data": { "id": "IDnAdd9kIAMCEsQ=", @@ -103,8 +103,8 @@ If enabled, all events are published to a Hotsock-specific event bus in your acc "disconnectedAt": null, "keepAlive": true, "sourceIp": "12.34.56.78", - "uid": null, - "umd": null, + "uid": "12345", + "umd": { "firstName": "Jim", "lastName": "Halpert" }, "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0" }, "dataType": "connection" @@ -125,7 +125,7 @@ The following is a sample SNS message for a newly established connection. Most o "Type": "Notification", "MessageId": "4360060f-c95a-58d5-8a7c-58dbecd688ed", "TopicArn": "arn:aws:sns:us-east-1:111111111111:Hotsock-PubSub-EOLFPNQLL8CW-Topic-AZXpBXfkywWc", - "Message": "{\"source\":\"hotsock.v1\",\"type\":\"hotsock.connected\",\"metadata\":{\"hotsockVersion\":\"1.9.1\"},\"data\":{\"id\":\"YppzrdWfoAMCJBQ=\",\"umd\":null,\"connectedAt\":\"2024-05-31T19:21:47.747616027Z\",\"disconnectedAt\":null,\"keepAlive\":true,\"uid\":null,\"userAgent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0\",\"sourceIp\":\"12.34.56.78\"},\"dataType\":\"connection\"}", + "Message": "{\"source\":\"hotsock.v1\",\"type\":\"hotsock.connected\",\"metadata\":{\"hotsockVersion\":\"1.13.0\"},\"data\":{\"id\":\"YppzrdWfoAMCJBQ=\",\"umd\":{\"firstName\":\"Jim\",\"lastName\":\"Halpert\"},\"connectedAt\":\"2024-05-31T19:21:47.747616027Z\",\"disconnectedAt\":null,\"keepAlive\":true,\"uid\":\"12345\",\"userAgent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0\",\"sourceIp\":\"12.34.56.78\"},\"dataType\":\"connection\"}", "Timestamp": "2024-05-31T19:21:48.428Z", "SignatureVersion": "1", "Signature": "PSnfnie3xEi4XDiJtcDBJGnQbZS5rvRut+ZAngBnQS7GqA/SC4fZuRFNf5d3pjoOuWM8idp5qyS+SppkBEt2a32cxEIeCMbV7MVeCR1A5FVuKKwZpqmmhyKIVfEvn30htjAgyy7V/OOvvzecoR0rHdG1KcyOze2XQILY6e8AAz8o/3mwpYx+KO7N1Ifs3+zavbjS/nuZbVyPVSxAWn2J9fcOGje/QhPi3wYvnrhsIotxPrLefIOWiRY3ayB5kvdkTTbRW1DADv/daCB9o7wpi9JNFQqklNkVOURNE2pjW/A5m9P1Ah14I437sS7xGHFeAfyBJhf4GwANS7r4Lyb/HQ==", @@ -155,7 +155,7 @@ This is always `hotsock.connected`. ```json { - "hotsockVersion": "1.9.1" + "hotsockVersion": "1.13.0" } ``` @@ -177,8 +177,8 @@ This is always `hotsock.connected`. "disconnectedAt": null, "keepAlive": true, "sourceIp": "12.34.56.78", - "uid": null, - "umd": null, + "uid": "12345", + "umd": { "firstName": "Jim", "lastName": "Halpert" }, "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0" } ``` @@ -201,7 +201,7 @@ This is always `hotsock.disconnected`. ```json { - "hotsockVersion": "1.9.1" + "hotsockVersion": "1.13.0" } ``` @@ -223,8 +223,8 @@ This is always `hotsock.disconnected`. "disconnectedAt": "2024-05-31T17:47:45.000000000Z", "keepAlive": true, "sourceIp": "12.34.56.78", - "uid": null, - "umd": null, + "uid": "12345", + "umd": { "firstName": "Jim", "lastName": "Halpert" }, "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0" } ``` @@ -249,7 +249,7 @@ This is always `hotsock.channelUpdated`. ```json { "channel": "my-channel", - "hotsockVersion": "1.9.1" + "hotsockVersion": "1.13.0" } ``` @@ -292,7 +292,7 @@ This is always `hotsock.messagePublished`. "channel": "my-channel", "connectionId": "IDnAdd9kIAMCEsQ=", "event": "my-event", - "hotsockVersion": "1.9.1", + "hotsockVersion": "1.13.0", "requestId": "9d1d380a-0152-4962-bbbb-6e721b81db52", "sourceIp": "12.34.56.78", "trigger": "client.websocket", @@ -337,7 +337,7 @@ This is always `hotsock.subscribed`. ```json { "channel": "my-channel", - "hotsockVersion": "1.9.1" + "hotsockVersion": "1.13.0" } ``` @@ -356,8 +356,8 @@ The data object type for this event is `subscription`. { "channel": "my-channel", "connectionId": "IDnAdd9kIAMCEsQ=", - "uid": null, - "umd": null + "uid": "12345", + "umd": { "firstName": "Jim", "lastName": "Halpert" } } ``` @@ -377,7 +377,7 @@ This is always `hotsock.unsubscribed`. ```json { "channel": "my-channel", - "hotsockVersion": "1.9.1" + "hotsockVersion": "1.13.0" } ``` @@ -392,8 +392,8 @@ This is always `hotsock.unsubscribed`. { "channel": "my-channel", "connectionId": "IDnAdd9kIAMCEsQ=", - "uid": null, - "umd": null + "uid": "12345", + "umd": { "firstName": "Jim", "lastName": "Halpert" } } ``` @@ -415,7 +415,7 @@ This is always `hotsock.connectionUpdated`. ```json { - "hotsockVersion": "1.12.0" + "hotsockVersion": "1.13.0" } ``` @@ -438,7 +438,7 @@ This is always `hotsock.connectionUpdated`. "keepAlive": true, "sourceIp": "12.34.56.78", "uid": "12345", - "umd": { "name": "Dwight", "status": "away" }, + "umd": { "firstName": "Jim", "lastName": "Halpert" }, "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0" } ``` @@ -473,7 +473,7 @@ SNS filter attributes are limited to `channel`, `key`, and `trigger`. { "channel": "my-channel", "connectionId": "IDnAdd9kIAMCEsQ=", - "hotsockVersion": "1.12.0", + "hotsockVersion": "1.13.0", "key": "status", "sourceIp": "12.34.56.78", "trigger": "client.websocket", @@ -499,10 +499,238 @@ SNS filter attributes are limited to `channel`, `key`, and `trigger`. "dataPrevious": "away", "expired": false, "expiresAt": "2026-04-03T12:00:00Z", - "meta": { "uid": "12345", "umd": null } + "meta": { "uid": "12345", "umd": { "firstName": "Jim", "lastName": "Halpert" } } } ``` ### `dataType` {/* #hotsock.channelStorageUpdated--dataType */} The data object type for this event is `channelStorage`. + +## `hotsock.messageReactionAdded` {/* #hotsock.messageReactionAdded */} + +This event is sent whenever a reaction is added to a stored message. Gated by the [`emitPubSubEvent`](../connections/claims.mdx#channels.messages.emitPubSubEvent) directive on the matching message event pattern. + +### `type` {/* #hotsock.messageReactionAdded--type */} + +This is always `hotsock.messageReactionAdded`. + +### `metadata` {/* #hotsock.messageReactionAdded--metadata */} + +- `channel` (String): The name of the channel where the reaction was added. +- `connectionId` (String): The identifier for the connection that added the reaction. Only present for client-initiated reactions. +- `hotsockVersion` (String): The version of the Hotsock installation that generated this event. +- `reaction` (String): The reaction value that was added. +- `requestId` (String): The request ID for the Lambda invocation that originally accepted the reaction. +- `sourceIp` (String): The IP address of the client. Only present if known. +- `trigger` (String): The kind of initiator, set to one of `client.websocket`, `client.http`, `server.lambda`, or `server.http`. +- `userAgent` (String): The User-Agent of the client. Only present if known. + +```json +{ + "channel": "my-channel", + "connectionId": "IDnAdd9kIAMCEsQ=", + "hotsockVersion": "1.13.0", + "reaction": "๐", + "requestId": "9d1d380a-0152-4962-bbbb-6e721b81db52", + "sourceIp": "12.34.56.78", + "trigger": "client.websocket", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0" +} +``` + +### `data` {/* #hotsock.messageReactionAdded--data */} + +- `channel` (String): The name of the channel where the reaction was added. +- `messageId` (String): The ID of the message that was reacted to ([ULID](https://github.com/ulid/spec)). +- `id` (String): The unique ID of this reaction ([ULID](https://github.com/ulid/spec)). +- `reaction` (String): The reaction value. +- `uid` (String): The user ID of the user who added the reaction. +- `umd` (JSON | null): The user metadata of the user who added the reaction. +- `createdAt` (String): The ISO3339 timestamp when the reaction was created. +- `expiresAt` (String | null): The RFC3339 timestamp when the reaction expires (inherits the parent message's TTL), or `null` if the message has no TTL. + +```json +{ + "channel": "my-channel", + "messageId": "01HZAD885RZ308CJM4YK825G65", + "id": "01HZADC44KZ308CJM4YK825G65", + "reaction": "๐", + "uid": "12345", + "umd": { "firstName": "Jim", "lastName": "Halpert" }, + "createdAt": "2026-05-19T15:47:45.231Z", + "expiresAt": "2027-05-19T15:47:45Z" +} +``` + +### `dataType` {/* #hotsock.messageReactionAdded--dataType */} + +The data object type for this event is `messageReaction`. + +## `hotsock.messageReactionRemoved` {/* #hotsock.messageReactionRemoved */} + +This event is sent whenever a reaction is explicitly removed from a stored message. TTL-driven reaction deletes (when a message expires) do not trigger this event. + +### `type` {/* #hotsock.messageReactionRemoved--type */} + +This is always `hotsock.messageReactionRemoved`. + +### `metadata` {/* #hotsock.messageReactionRemoved--metadata */} + +:::note +`connectionId`, `requestId`, `sourceIp`, `trigger`, and `userAgent` reflect the original request that *added* the reaction, not the request that triggered the removal. +::: + +- `channel` (String): The name of the channel where the reaction was removed. +- `connectionId` (String): The identifier for the connection that originally added the reaction. Only present for client-initiated reactions. +- `hotsockVersion` (String): The version of the Hotsock installation that generated this event. +- `reaction` (String): The reaction value that was removed. +- `requestId` (String): The request ID for the Lambda invocation that originally added the reaction. +- `sourceIp` (String): The IP address of the client that originally added the reaction. Only present if known. +- `trigger` (String): The kind of initiator that originally added the reaction, set to one of `client.websocket`, `client.http`, `server.lambda`, or `server.http`. +- `userAgent` (String): The User-Agent of the client that originally added the reaction. Only present if known. + +```json +{ + "channel": "my-channel", + "connectionId": "IDnAdd9kIAMCEsQ=", + "hotsockVersion": "1.13.0", + "reaction": "๐", + "requestId": "9d1d380a-0152-4962-bbbb-6e721b81db52", + "sourceIp": "12.34.56.78", + "trigger": "client.websocket", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0" +} +``` + +### `data` {/* #hotsock.messageReactionRemoved--data */} + +- `channel` (String): The name of the channel where the reaction was removed. +- `messageId` (String): The ID of the message that was reacted to ([ULID](https://github.com/ulid/spec)). +- `id` (String): The unique ID of this reaction ([ULID](https://github.com/ulid/spec)). +- `reaction` (String): The reaction value. +- `uid` (String): The user ID of the user who removed the reaction. +- `umd` (JSON | null): The user metadata of the user who removed the reaction. +- `createdAt` (String): The ISO3339 timestamp when the reaction was originally created. +- `expiresAt` (String | null): The RFC3339 timestamp when the reaction would have expired, or `null` if the message has no TTL. + +```json +{ + "channel": "my-channel", + "messageId": "01HZAD885RZ308CJM4YK825G65", + "id": "01HZADC44KZ308CJM4YK825G65", + "reaction": "๐", + "uid": "12345", + "umd": { "firstName": "Jim", "lastName": "Halpert" }, + "createdAt": "2026-05-19T15:47:45.231Z", + "expiresAt": "2027-05-19T15:47:45Z" +} +``` + +### `dataType` {/* #hotsock.messageReactionRemoved--dataType */} + +The data object type for this event is `messageReaction`. + +## `hotsock.memberAdded` {/* #hotsock.memberAdded */} + +This event is sent whenever a unique member is added to a [presence channel](../channels/presence.mdx). Member events are deduplicated by `uid` to match WebSocket fan-out semantics โ if a user is already present on the channel from another connection, no `hotsock.memberAdded` event fires when an additional connection joins. + +### `type` {/* #hotsock.memberAdded--type */} + +This is always `hotsock.memberAdded`. + +### `metadata` {/* #hotsock.memberAdded--metadata */} + +- `channel` (String): The name of the presence channel the member joined. +- `hotsockVersion` (String): The version of the Hotsock installation that generated this event. + +```json +{ + "channel": "presence.chat", + "hotsockVersion": "1.13.0" +} +``` + +### `data` {/* #hotsock.memberAdded--data */} + +- `uid` (String): The user ID of the member that was added. +- `umd` (JSON | null): The user metadata of the member that was added. +- `members` (Array): The full deduplicated member list for the channel after the change. + +```json +{ + "uid": "Dwight", + "umd": { "status": "available" }, + "members": [ + { "uid": "Jim", "umd": null }, + { "uid": "Dwight", "umd": { "status": "available" } } + ] +} +``` + +### `dataType` {/* #hotsock.memberAdded--dataType */} + +The data object type for this event is `channelMember`. + +## `hotsock.memberRemoved` {/* #hotsock.memberRemoved */} + +This event is sent whenever the last subscription for a `uid` on a [presence channel](../channels/presence.mdx) ends. If the same user is still subscribed from another connection, no `hotsock.memberRemoved` event fires. + +### `type` {/* #hotsock.memberRemoved--type */} + +This is always `hotsock.memberRemoved`. + +### `metadata` {/* #hotsock.memberRemoved--metadata */} + +Same as [`hotsock.memberAdded` metadata](#hotsock.memberAdded--metadata). + +### `data` {/* #hotsock.memberRemoved--data */} + +- `uid` (String): The user ID of the member that was removed. +- `umd` (JSON | null): The last-known user metadata of the member that was removed. +- `members` (Array): The full deduplicated member list for the channel after the change. + +```json +{ + "uid": "Dwight", + "umd": { "status": "available" }, + "members": [{ "uid": "Jim", "umd": null }] +} +``` + +### `dataType` {/* #hotsock.memberRemoved--dataType */} + +The data object type for this event is `channelMember`. + +## `hotsock.memberUpdated` {/* #hotsock.memberUpdated */} + +This event is sent whenever a presence channel member's user metadata changes โ either through a [`hotsock.umdUpdate`](../connections/claims.mdx#channels.umdUpdate) command, a connection-level [`umdUpdate`](../connections/claims.mdx#umdUpdate) with [`umdPropagate`](../connections/claims.mdx#umdPropagate), or a duplicate `uid` subscribing with different `umd`. + +### `type` {/* #hotsock.memberUpdated--type */} + +This is always `hotsock.memberUpdated`. + +### `metadata` {/* #hotsock.memberUpdated--metadata */} + +Same as [`hotsock.memberAdded` metadata](#hotsock.memberAdded--metadata). + +### `data` {/* #hotsock.memberUpdated--data */} + +- `uid` (String): The user ID of the member whose metadata changed. +- `umd` (JSON | null): The updated user metadata. +- `members` (Array): The full deduplicated member list for the channel, reflecting the updated metadata. + +```json +{ + "uid": "Dwight", + "umd": { "status": "away" }, + "members": [ + { "uid": "Jim", "umd": null }, + { "uid": "Dwight", "umd": { "status": "away" } } + ] +} +``` + +### `dataType` {/* #hotsock.memberUpdated--dataType */} + +The data object type for this event is `channelMember`. diff --git a/src/components/HomepageBanner.js b/src/components/HomepageBanner.js index 9822598..a0de3c2 100644 --- a/src/components/HomepageBanner.js +++ b/src/components/HomepageBanner.js @@ -8,17 +8,17 @@ export default function HomepageBanner() {
- New in v1.12: channel storage and live user metadata updates! + New in v1.13: message reactions and presence pub/sub! - New in v1.12: persistent channel storage with real-time sync and - live user metadata updates without reconnecting! + New in v1.13: message reactions, presence events on pub/sub, + and server-enforced heartbeats!