Go SDK events, sync and Store
The Go SDK receives server events over one WebSocket connection, keeps a local cache of conversations and messages in an in-memory Store (c.Store()), and does a full sync with SyncAll after sign-in. This page covers the event model, every event, the sync flow, how to read the Store, and error handling.
For installing, pinning versions and local source replacement, see Quickstart.
| You want to | See |
|---|---|
| Receive events, and know which goroutine callbacks run on | Event model |
| Look up an event's name and payload | All events |
| Load conversations and messages after sign-in, scroll back through history | Sync after sign-in |
| Read conversations and messages from the in-memory cache | Store |
| Handle failures by error code | Errors |
Event model
Once the connection is open (see ConnectWait in Initialization and sign-in), register a callback to receive every event:
c.OnEvent(func(e im.Event) {
switch e.Name {
case im.EventMessageReceived:
var m im.Message
if json.Unmarshal(e.Data, &m) == nil {
fmt.Println(m.Sender, m.TextBody())
}
}
})im.EventhasName(the event name, such asmessage.received),ID(the event ID, for de-duplication),At(the event time in RFC 3339; parse it withe.Time()) andData(the payload asjson.RawMessage; parse it per event).- You can register several callbacks; they're called in registration order. A panic in a callback is recovered by the SDK and doesn't affect the connection.
- Callbacks run synchronously on the goroutine that reads the connection: until a callback returns, later events and replies to WS requests wait. Don't do slow work in a callback, and don't call methods that go over the connection, such as
SendText, directly (the reply they wait for can only be read after the callback returns, so they fall back to REST after timing out). Start your own goroutine when needed. - Each event is applied to the Store first (ordered and de-duplicated), then passed to you, so reading
c.Store()in the callback already reflects that event. - The server may add event types. Event names the SDK doesn't know don't cause errors and are passed through as is; parse
e.Dataor ignore them.
Connection state changes have their own c.OnState(fn func(im.ConnectionState)), with states StateDisconnected, StateConnecting and StateConnected (authenticated); see Initialization and sign-in.
All events
The event name constants are all in the im package, grouped by prefix. The payload column gives the SDK's defined struct; events marked "not modeled" have no payload struct in the SDK, so parse e.Data yourself.
Messages and conversations
| Constant | Event name | Meaning | Payload |
|---|---|---|---|
im.EventMessageReceived | message.received | A new message arrived (not echoed to its sender) | im.Message (inside im.MessageEventData) |
im.EventMessageChanged | message.changed | A message changed: recalled, edited, reacted to, pinned and so on; only the changed fields | im.MessageChangedData |
im.EventMessageDeleted | message.deleted | Messages were deleted (a list of seqs) | im.MessageDeletedData |
im.EventMessageOnline | message.online | An online-only message that isn't stored | im.Message |
im.EventConversationChanged | message.conversation_changed | Conversation state changed; data may also be {"resync":true}, in which case the SDK syncs again automatically | im.ConversationState or the resync instruction |
im.EventPeerRead | message.peer_read | The other person's read position in a one-to-one chat moved forward | im.PeerReadData |
im.EventReadBy | message.read_by | Read details for group messages | im.ReadByData |
im.EventTyping | message.typing | Typing indicator | im.TypingData |
im.EventSyncHint | message.sync_hint | The server signals new data; the SDK does an incremental sync automatically | im.SyncHintData |
Change.Kind in im.MessageChangedData is recalled, edited, erased, reaction_added, reaction_removed, pinned or unpinned, and the changed values appear in the matching fields (Recalled, Edited, Reactions, Pinned and so on).
Friends and blocklist
| Constant | Event name | Meaning | Payload |
|---|---|---|---|
im.EventFriendAdded | friend.added | A friend was added | im.FriendEventData |
im.EventFriendUpdated | friend.updated | A friend's profile or remark changed | im.FriendEventData |
im.EventFriendRemoved | friend.removed | A friend was removed (Reason says why) | im.FriendEventData |
im.EventFriendRequestReceived | friend_request.received | A friend request arrived | im.FriendRequestEventData |
im.EventFriendRequestDeclined | friend_request.declined | A sent request was declined | im.FriendRequestEventData |
im.EventFriendRequestRead | friend_request.read | The request list's read position moved forward (from another device) | im.FriendRequestReadData |
im.EventBlacklistChanged | blacklist.changed | The blocklist changed; Blocked false means unblocked | im.BlacklistChangedData |
Groups
| Constant | Event name | Meaning | Payload |
|---|---|---|---|
im.EventGroupCreated | group.created | A new group (created, or the user was added) | im.GroupEventData |
im.EventGroupInfoChanged | group.info_changed | Group info changed (Info is the full group info, ChangedFields lists what changed) | im.GroupEventData |
im.EventGroupMembersAdded | group.members_added | Members joined | im.GroupEventData |
im.EventGroupMembersRemoved | group.members_removed | Members were removed (Reason) | im.GroupEventData |
im.EventGroupMembersUpdated | group.members_updated | Member roles and the like changed (Role) | im.GroupEventData |
im.EventGroupDismissed | group.dismissed | The group was dissolved | im.GroupEventData |
im.EventGroupRequestCreated | group.request_created | A new join request or invitation | im.GroupEventData |
im.EventGroupRequestHandled | group.request_handled | A join request or invitation was handled | im.GroupEventData |
Users, presence and system
| Constant | Event name | Meaning | Payload |
|---|---|---|---|
im.EventUserProfileChanged | user.profile_changed | The user's own profile changed | im.UserProfileChangedData |
im.EventUserSessionsRevoked | user.sessions_revoked | The user's sign-ins were revoked | Not modeled |
im.EventUserReloginRequired | user.relogin_required | The user must sign in again | Not modeled |
im.EventUserStatusChanged | user.status_changed | User status changed | Not modeled |
im.EventUserMuteChanged | user.mute_changed | The user's mute status changed | Not modeled |
im.EventPresenceChanged | presence.changed | A subscribed person's presence changed (see Users, friends and presence) | Same structure as im.PresenceItem |
im.EventPresenceReset | presence.reset | The server cancelled the subscriptions; clear local state and subscribe again | Not modeled |
im.EventDevicesChanged | devices.changed | The user's device list changed | Not modeled |
im.EventConfigChanged | config.changed | App config changed | Not modeled |
im.EventAuthExpired | auth.expired | Credentials expired | Not modeled |
im.EventSyncRequired | sync.required | The server requires a resync; the SDK syncs again automatically | Not modeled |
im.EventPolicyChanged | policy.changed | App runtime policies changed | Not modeled |
Sync after sign-in
c.SyncAll(ctx) does a full sync, returns the number of conversations synced, and writes everything to the Store:
- recent: fetches the recent conversation list (including pinned ones) and writes each with its last message to the Store. The version the first page gives is the starting point for the incremental step.
- group-digest: pages through state summaries of all group conversations.
- Incremental sync: from the version recent gave, fetches changed conversation states and deleted messages. If the server says a full rerun is needed, the SDK reruns everything (once only, to avoid a loop).
- last-messages: for conversations whose last message recent didn't include, fetches the last message one by one.
ConnectWait doesn't sync for you
The SDK doesn't call SyncAll itself after connecting. After signing in and ConnectWait succeeds, call c.SyncAll(ctx) once to load the conversation list and messages, then read the Store to render. After that the SDK keeps things up to date incrementally: on sync_hint, sync.required or conversation events asking for a resync, it reruns the sync in the background (with a 30-second timeout), with nothing needed from you.
To fetch more history, use c.FetchMessages(conversationID, afterSeq, beforeSeq, limit): afterSeq > 0 fills the gap after that seq, beforeSeq > 0 scrolls back through history, and limit <= 0 uses the default of 50. c.History(conversationID, beforeSeq, limit) is shorthand for scrolling back. Both write their results to the Store.
Store
c.Store() returns a thread-safe in-memory cache (with an internal read-write lock). Read methods return snapshot copies, so you can iterate them freely without interfering with event handling:
| Method | Use |
|---|---|
s.Self() | The current username |
s.Conversation(id) | A conversation's state (*im.ConversationState); nil if not present |
s.Conversations() | All conversations, newest message first |
s.Messages(conversationID) | Cached messages in a conversation, ascending by seq |
s.LastMessages(conversationID, limit) | The latest limit messages in a conversation (ascending) |
s.Typing(conversationID) | The latest typing indicator (valid for 10 seconds, otherwise nil) |
The write methods PutConversation, PutMessage and DeleteMessages are for the sync flow and REST fetches; you usually don't call them.
The Store applies events in order. For message.received: if seq is the local max plus one it's appended; if it's larger there's a gap, so the message is accepted and the gap fetched in the background; if it's smaller it's ignored. When a conversation's cleared_seq grows, local messages up to it are deleted automatically. An event ID is never applied twice. Senders don't get their own message.received, so after a successful send the SDK creates or advances the conversation's state locally.
In-memory Store, empty after a restart
The Store lives only in memory and is empty after a process restart; sign in again and call SyncAll to restore it. Token persistence (Config.TokenStorage) only saves you from signing in again after a restart; it doesn't keep Store data. Friends, groups and members aren't cached in the Store; persist them yourself if you need them locally.
Errors
Business errors from REST and the connection are all *im.APIError:
| Field | Description |
|---|---|
Code | Machine-readable error code, such as unauthenticated or not_found |
Message | Human-readable description |
Details | Extra details (json.RawMessage), such as {"reason":"token_expired"} |
RequestID | The server's troubleshooting ID |
HTTPStatus | HTTP status for REST; 0 for connection errors |
Check the code with im.IsCode, and get details.reason with Reason(). Local failures, such as the connection not being ready or a WS request timing out, also come back as *im.APIError (with code unavailable or timeout).
_, err := c.UpdateGroup(ctx, id, &name, nil, nil)
if err != nil {
switch {
case im.IsCode(err, im.CodePermission):
fmt.Println("没有权限")
case im.IsCode(err, im.CodeNotFound):
fmt.Println("群不存在")
default:
fmt.Println(err) // 形如 im: forbidden: xxx [request_id ...]
}
}Error code constants you can check with im.IsCode:
| Constant | Error code | Meaning |
|---|---|---|
im.CodeUnauthenticated | unauthenticated | Not signed in or credentials invalid (the SDK first tries renewing once with the refresh token) |
im.CodeForbidden | forbidden | Access forbidden |
im.CodeNotFound | not_found | The resource doesn't exist or isn't visible |
im.CodeInvalidArgument | invalid_argument | Invalid parameters |
im.CodeVersionConflict | version_conflict | Version conflict |
im.CodeRateLimited | rate_limited | Too many requests |
im.CodePermission | permission_denied | No permission, or the feature isn't turned on |
im.CodeInternal | internal | Internal server error |
im.CodeUnavailable | unavailable | Service temporarily unavailable (including in-flight requests failing when the connection drops) |
Also, c.SessionValid() reports whether the client holds an access token (it doesn't check expiry; an expired one is renewed automatically on the next request). For automatic token renewal and how reconnects depend on close codes, see Initialization and sign-in.
See also
- Initialization and sign-in: create a client, sign in, connection state and automatic reconnects.
- Messages: send and manage messages, read messages from the Store.
- Conversations: conversation state fields, pinning and other operations.
