feat(websocket): add event helper package#1917
Conversation
9cbeb51 to
af8bd91
Compare
There was a problem hiding this comment.
Code Review
This pull request introduces a new event subpackage to the Fiber WebSocket middleware, providing an event-bus helper for plain WebSockets with features like connection pooling and event-driven communication. The code review identified several significant issues: the read method's polling mechanism is inefficient and potentially blocks context cancellation, the Close method fails to trigger proper resource cleanup, and the retry logic in the send goroutine may compromise message ordering. Furthermore, unsafe type assertions in attribute retrieval methods pose a risk of runtime panics, and the reviewer suggested simplifying slice copying logic for better readability.
af8bd91 to
66bed97
Compare
|
/gemini review |
|
@copilot review |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Code Review
This pull request introduces a new v3/websocket/event package providing a plain WebSocket event-bus helper, alongside a v3/socketio/legacy shim to facilitate migration for users of older Socket.IO implementations. It also refactors the base websocket package to safely manage hijacked connections. Review feedback highlights several critical areas for improvement: potential runtime panics in attribute getters due to unsafe type assertions, a race condition in the legacy configuration synchronization, and performance overhead in the read loop caused by unnecessary polling. Additionally, it is recommended to make the message queue size configurable and to optimize the retry logic to prevent excessive goroutine spawning.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📜 Recent review details⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
🔇 Additional comments (4)
📝 WalkthroughWalkthroughImplements a plain WebSocket event-bus in ChangesWebSocket Event-Bus, Legacy Compatibility, and Documentation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
v3/websocket/event/event.go (1)
526-563: 🏗️ Heavy liftTicker-based read loop is inefficient.
The
readfunction uses a ticker withReadTimeout(10ms default) to poll for messages, which burns CPU cycles and introduces latency. WebSocketReadMessageis blocking and can be interrupted withSetReadDeadlineor by closing the connection. Consider blocking onReadMessagedirectly and using context cancellation or connection closure to exit the loop.Possible approach (blocking ReadMessage)
func (kws *Websocket) read(ctx context.Context) { for { if !kws.hasConn() { select { case <-ctx.Done(): return case <-time.After(ReadTimeout): continue } } mType, msg, err := kws.Conn.ReadMessage() // blocking // Handle context cancellation by checking after read select { case <-ctx.Done(): return default: } if mType == PingMessage { kws.fireEvent(EventPing, nil, nil) continue } // ... rest of handling } }This approach blocks on
ReadMessageand only checks for context cancellation when needed, significantly reducing CPU usage.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@v3/websocket/event/event.go` around lines 526 - 563, The read loop currently polls with a ticker; replace it with a blocking ReadMessage loop in the read method: stop using timeoutTicker, loop calling kws.Conn.ReadMessage() directly (but only when kws.hasConn() is true), handle PingMessage/PongMessage/CloseMessage and error exactly as now (calling kws.fireEvent or kws.disconnected), and use context cancellation to break the loop by either setting a read deadline via Conn.SetReadDeadline(ctxDeadline) each iteration or by closing the connection from another goroutine when ctx is done so ReadMessage unblocks; keep checks to skip reads when !kws.hasConn() and still return on ctx.Done(). Ensure you update references to read, hasConn, Conn.ReadMessage, SetReadDeadline (or connection close), fireEvent and disconnected accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@v3/socketio/legacy/legacy_test.go`:
- Around line 40-50: The test starts the server in a goroutine using
app.Listener(ln) but immediately attempts a client Dial via
websocket.Dialer.Dial("ws://"+ln.Addr().String(), nil), causing a race; ensure
the client waits for the server to be ready by adding synchronization—e.g. have
the goroutine signal on a ready channel (send after app.Listener(ln) returns or
once the listener is accepting) and block the test until that channel is
closed/received, or implement a short retry/backoff loop around dial attempts
(retry websocket.Dialer.Dial using ln.Addr().String() until success with a
timeout); reference the goroutine that calls app.Listener(ln) and the dial call
that invokes websocket.Dialer.Dial to locate where to add the
synchronization/retry.
- Around line 8-12: The dependency github.com/gofiber/contrib/v3/websocket
(imported as fws in the test imports) is pinned to v1.0.0 and must be updated to
v1.1.2; open go.mod and change the module version for
github.com/gofiber/contrib/v3/websocket to v1.1.2, then run go get
github.com/gofiber/contrib/v3/websocket@v1.1.2 and go mod tidy to update the
lockfile and ensure imports (e.g., the fws alias in legacy_test.go) continue to
resolve against the new version.
In `@v3/websocket/event/event.go`:
- Around line 320-329: GetStringAttribute currently does an unchecked type
assertion value.(string) which can panic if the stored attribute isn't a string;
change it to use a checked assertion (s, ok := value.(string)) after reading
from kws.attributes inside the mutex-protected block and return s only if ok is
true, otherwise return the empty string (optionally consider logging the type
mismatch), referencing the GetStringAttribute method, kws.attributes map and
kws.mu mutex.
- Around line 309-318: GetIntAttribute currently does an unchecked type
assertion value.(int) which can panic if kws.attributes[key] holds a non-int;
change it to use a checked assertion (v, ok := value.(int)) and return v when
ok, otherwise return 0 (or a default) to avoid panics — update the
GetIntAttribute method on the Websocket type that reads from kws.attributes
(while holding kws.mu.RLock) to perform the safe assertion and handle the
mismatch gracefully (optionally log or comment the fallback).
---
Nitpick comments:
In `@v3/websocket/event/event.go`:
- Around line 526-563: The read loop currently polls with a ticker; replace it
with a blocking ReadMessage loop in the read method: stop using timeoutTicker,
loop calling kws.Conn.ReadMessage() directly (but only when kws.hasConn() is
true), handle PingMessage/PongMessage/CloseMessage and error exactly as now
(calling kws.fireEvent or kws.disconnected), and use context cancellation to
break the loop by either setting a read deadline via
Conn.SetReadDeadline(ctxDeadline) each iteration or by closing the connection
from another goroutine when ctx is done so ReadMessage unblocks; keep checks to
skip reads when !kws.hasConn() and still return on ctx.Done(). Ensure you update
references to read, hasConn, Conn.ReadMessage, SetReadDeadline (or connection
close), fireEvent and disconnected accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 63d4d93c-f650-44ba-8a0a-2524226f253c
⛔ Files ignored due to path filters (2)
v3/websocket/go.modis excluded by!**/*.modv3/websocket/go.sumis excluded by!**/*.sum,!**/*.sum
📒 Files selected for processing (9)
v3/socketio/README.mdv3/socketio/legacy/README.mdv3/socketio/legacy/legacy.gov3/socketio/legacy/legacy_test.gov3/websocket/README.mdv3/websocket/event/README.mdv3/websocket/event/event.gov3/websocket/event/event_test.gov3/websocket/websocket.go
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: copilot
🧰 Additional context used
🪛 LanguageTool
v3/socketio/legacy/README.md
[grammar] ~5-~5: Ensure spelling is correct
Context: --- id: socketio-legacy --- # SocketIO Legacy Event Shim Compatibility shim f...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
🔇 Additional comments (23)
v3/websocket/websocket.go (2)
82-95: LGTM!
159-159: LGTM!v3/websocket/event/event.go (9)
16-69: LGTM!
71-143: LGTM!
145-224: LGTM!
226-257: LGTM!
259-266: LGTM!Also applies to: 291-307
267-289: LGTM!
331-421: LGTM!
473-500: LGTM!
422-446: LGTM!Also applies to: 447-458, 460-471, 502-524, 565-634
v3/websocket/event/README.md (1)
1-95: LGTM!v3/websocket/README.md (1)
13-14: LGTM!v3/websocket/event/event_test.go (1)
1-397: LGTM!v3/socketio/legacy/legacy.go (1)
1-107: LGTM!v3/socketio/legacy/README.md (1)
1-34: LGTM!v3/socketio/README.md (1)
13-14: LGTM!v3/socketio/legacy/legacy_test.go (6)
1-13: LGTM!
15-22: LGTM!
24-29: LGTM!
44-52: LGTM!
54-59: LGTM!
31-34: All required symbols are properly exported from the legacy package.The verification confirms that
On,EventPayload,Websocket, andTextMessageare all exported fromv3/socketio/legacy/legacy.go(as either type aliases or constants). These symbols are correctly accessible within the test file without explicit imports since the test shares the same package scope.
Reviewed and addressed in af4409d. Here's a summary of what was found and fixed: Bugs fixed:
Tests updated to cover safe type coercion: Design notes (not changed):
|
70b3432 to
4e85f0a
Compare
…move dead code, deduplicate EmitTo checks Agent-Logs-Url: https://github.com/gofiber/contrib/sessions/1bdf2b2e-f13d-47c1-b47e-63079573f6d0 Co-authored-by: ReneWerner87 <7063188+ReneWerner87@users.noreply.github.com>
4e85f0a to
b0e9c2c
Compare
…ettings Adds an event.Config struct and event.NewWithConfig entry point so callers can configure ping interval, idle timeout, write timeout, max message size, send queue size, send retries and a recover handler per instance. The existing event.New keeps its signature and delegates to NewWithConfig with a zero Config. At connection upgrade the package vars (PongTimeout, RetrySendTimeout, MaxSendRetry, SendQueueSize) are read exactly once into an immutable per-connection settings snapshot. Goroutines (pong, send) now read kws.settings instead of the bare package vars, so later mutations of the globals can no longer race with running connections. The done channel buffer of 1 is dropped: close() works regardless and a buffered signal channel is misleading. No behavior change for existing callers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A panic in a user On() callback used to abort the read or send goroutine before disconnected() ran, leaving the pool entry and worker goroutines behind. The pooled *Conn could then be recycled while the helper still held a reference. Now fireEvent invokes every callback through a small trampoline that recovers; the optional Config.RecoverHandler receives the event name and recovered value for telemetry. While here, clone the inbound []byte once before fan-out so listeners that retain payload.Data are not exposed to the read buffer being reused for the next frame. Adds TestListenerPanicIsRecovered and TestEventPayloadDataIsIndependentOfReadBuffer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before this change ReadMessage was called without SetReadLimit, so a single client could stream a multi-GB frame and OOM the process. run() now applies kws.settings.maxMessageSize to the underlying *Conn before any read happens. The default is 1 MiB; callers opt in to larger frames via Config.MaxMessageSize. Adds TestReadLimitRejectsOversizedFrame which configures a 16-byte cap and asserts EventDisconnect with a non-nil error after a 1 KiB write. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… on Pong
The old pong() goroutine sent unsolicited Pong frames on a ticker, which is
neither a Ping nor a real heartbeat: dead peers were only detected when
the next write attempt failed, and many proxies (Cloudflare, Envoy) reset
their idle timers only on data or Ping frames. The read loop had no
deadline at all, so an idle connection wedged three goroutines forever.
This commit:
- rewrites pong() to send PingMessage via WriteControl with a write
deadline (and exits on done so it cannot outlive the connection);
- installs SetPongHandler that refreshes the read deadline and fires
EventPong, plus SetPingHandler that mirrors the peer's data into a
Pong response and fires EventPing;
- sets ReadDeadline at run() start so a silent peer surfaces a timeout
error and gets disconnected;
- rewrites read() to drop the dead PingMessage/PongMessage dispatch
branches (control frames go through the library hooks), distinguishes
orderly close (1000/1001) from transport errors via IsCloseError, and
polls ctx/done on each iteration.
PongTimeout keeps its name for backwards compatibility but its godoc is
rewritten and marked Deprecated in favour of Config.PingInterval.
Adds TestPingIsSentAtInterval and TestReadDeadlineFiresDisconnectOnSilentPeer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…shutdown order
Three related transport fixes that all touch the close path:
- send() now sets a write deadline before each WriteMessage and, on
error, drains the queue and disconnects rather than spinning. Without
this, a slow reader could wedge the send goroutine forever while
Broadcast/EmitTo from other connections kept enqueuing.
- Close() no longer writes a non-RFC payload of "Connection closed"
bytes directly. RFC 6455 §5.5.1 mandates that close frames start with
a uint16 status code; peers MAY drop the connection with 1002
otherwise. A new writeClose helper formats the frame via
FormatCloseMessage, caps the reason to 123 bytes (125 control-frame
limit minus 2 for the code), and uses WriteControl with a deadline so
shutdown bypasses the queue entirely.
- run() reorders shutdown to <-done -> cancelFunc -> wg.Wait ->
closeConn. Previously closeConn raced with an in-flight WriteMessage
in the send goroutine. closeConn now nils kws.Conn under the mutex so
accidental external use panics on a clearly invalid value instead of
corrupting a recycled pooled *Conn.
Adds TestCloseSendsFormatCloseMessage and TestCloseConnNilsConnField.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A Fiber app.Shutdown previously hung on the event helper because every
upgrade handler is blocked in run() until the connection's done channel
closes, and nothing iterated the in-process pool to send a server-initiated
close. Clients saw a TCP reset rather than a 1001 Going Away frame.
This commit adds:
- event.Drain() / event.IsDraining() so an upgrade middleware can
cooperatively refuse new connections during a graceful shutdown;
- event.CloseAll(ctx, code, reason) that walks the pool snapshot, sends
the requested close frame via writeClose, fires EventClose, then
waits for run() to exit. If ctx expires first, every remaining
connection is force-closed via closeConn and ctx.Err is returned.
README documents the Fiber Hooks.OnShutdown wiring and the new Config
knobs introduced in earlier commits (PingInterval, ReadIdleTimeout,
WriteTimeout, MaxMessageSize, SendQueueSize, MaxSendRetry,
RetrySendTimeout, RecoverHandler).
Adds TestDrainFlag and TestCloseAllSendsGoingAway.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a TestMain that runs goleak.VerifyTestMain against the event helper test suite so regressions in connection cleanup (panic recovery, read deadline, send-side shutdown ordering) are caught instead of slipping through. Known fasthttp worker pool and server-date refresher goroutines park in time.Sleep after Server.Shutdown returns, so they are matched by IgnoreAnyFunction rather than IgnoreTopFunction. Verified locally with go test -race -count=3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the previous P1 commits, three helpers became unreachable:
- (*Websocket).tryWrite: only used by the old Close() path that has
been replaced by writeClose using WriteControl;
- sendQueueSize: replaced by per-connection settings snapshot in the
settings refactor;
- undrain: only the tests reset the drain flag, so they now do it
inline with draining.Store(false).
golangci-lint v2.6.2 with --tests=false flagged all three. Removing
them keeps the lint job clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
goleak is a direct test dependency of v3/websocket/event, not indirect, and the v3/socketio module sees it via the local replace directive on v3/websocket. go mod tidy in both modules surfaces these as the only diff. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new event subpackage under v3/websocket that provides a plain WebSocket event-bus helper (extracted from the legacy socketio package as a re-architected, configurable, leak-tested implementation), plus a thin deprecated compatibility shim at v3/socketio/legacy so existing consumers can migrate. Also adds a small helper in the core websocket middleware so KeepHijackedConns is enabled exactly once per fasthttp.Server and updates README/docs to point users to the new package.
Changes:
- New
v3/websocket/eventpackage implementing per-connection settings, ping/pong liveness, send queue with retry, gracefulDrain/CloseAll, and panic-recovering listeners, with comprehensive tests usinggoleak. - New
v3/socketio/legacyshim re-exporting the event API for backwards compatibility, plus README updates redirecting users. - Concurrency-safe
ensureKeepHijackedConns(server)helper replacing the inline read-modify-write onserver.KeepHijackedConns; minor refactor ofsocketio/polling.gocookie iteration; go.mod/sum updates forgoleakandgoogle/uuid.
Reviewed changes
Copilot reviewed 12 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| v3/websocket/websocket.go | Adds ensureKeepHijackedConns map+mutex helper and routes the upgrade handler through it. |
| v3/websocket/README.md | Points readers to the new event subpackage. |
| v3/websocket/go.mod / go.sum | Promotes google/uuid to direct dep, adds goleak and stretchr/objx. |
| v3/websocket/event/event.go | New event-bus implementation with Config, lifecycle, pool, listeners, graceful shutdown. |
| v3/websocket/event/event_test.go | Extensive tests including goleak, parallel dialers, ping/read-deadline, panic recovery. |
| v3/websocket/event/README.md | Documentation for the new package. |
| v3/socketio/README.md | Migration note pointing to the new event package and legacy shim. |
| v3/socketio/legacy/legacy.go | Thin deprecated alias layer re-exporting wsevent types/functions. |
| v3/socketio/legacy/legacy_test.go | Smoke test verifying the shim round-trips a message. |
| v3/socketio/legacy/README.md | Migration documentation for the shim. |
| v3/socketio/polling.go | Replaces VisitAllCookie callback with Cookies() range iterator. |
| v3/socketio/go.mod / go.sum | Bumps websocket dep version and adds goleak. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@v3/websocket/event/event.go`:
- Around line 940-947: In the ctx.Done() branch inside CloseAll, after casting
to *Websocket and calling kws.closeConn(), also mark the connection as
disconnected so it isn't considered alive or left in the pool: set kws.isAlive =
false (or call the Websocket disconnect/mark method if one exists) and remove
its UUID from pool (e.g., call pool.remove/erase using the Websocket's ID) so
subsequent emits won't treat the closed socket as alive.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2c2d7602-26cc-4305-b0be-53593fdb4e9b
⛔ Files ignored due to path filters (4)
v3/socketio/go.modis excluded by!**/*.modv3/socketio/go.sumis excluded by!**/*.sum,!**/*.sumv3/websocket/go.modis excluded by!**/*.modv3/websocket/go.sumis excluded by!**/*.sum,!**/*.sum
📒 Files selected for processing (10)
v3/socketio/README.mdv3/socketio/legacy/README.mdv3/socketio/legacy/legacy.gov3/socketio/legacy/legacy_test.gov3/socketio/polling.gov3/websocket/README.mdv3/websocket/event/README.mdv3/websocket/event/event.gov3/websocket/event/event_test.gov3/websocket/websocket.go
✅ Files skipped from review due to trivial changes (4)
- v3/socketio/polling.go
- v3/socketio/README.md
- v3/socketio/legacy/README.md
- v3/websocket/README.md
🚧 Files skipped from review as they are similar to previous changes (3)
- v3/websocket/websocket.go
- v3/socketio/legacy/legacy_test.go
- v3/websocket/event/README.md
📜 Review details
🔇 Additional comments (1)
v3/websocket/event/event_test.go (1)
346-346: No action needed – Go version 1.25.0 supports range-over-integer loops.The module's
godirective specifies 1.25.0, which fully supports the range-over-integer syntax used in this code. No changes required.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a new plain WebSocket event helper subpackage websocket/event to handle legacy event-bus behavior, along with a compatibility shim in socketio/legacy to ease migration. It also updates dependencies and introduces thread-safe server configuration in websocket.go. However, a critical compilation and logic error was identified in v3/socketio/polling.go where iterating over Cookies() incorrectly treats the loop variables as key-value pairs instead of index and struct, which must be resolved.
…-helper # Conflicts: # v3/socketio/go.mod # v3/socketio/go.sum # v3/websocket/go.mod # v3/websocket/go.sum
- Broadcast and disconnected now read the UUID via GetUUID() under the read lock instead of touching kws.UUID directly, removing a data race with SetUUID. - CloseAll's force-close path marks each connection disconnected before Conn.Close so a timed-out shutdown cannot leave stale alive entries in the pool. - SetUUID validates duplication before mutating kws.UUID, dropping the rollback dance. - Clarify EmitTo/EmitToList godocs: the method forms fire EventError, the package-level forms do not. - Rewrite TestWebsocketCloseDoesNotBlockOnFullQueue to exercise write() on a full queue after Close, the path that actually interacts with the queue. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses a second review round focused on documentation and the global listener footgun. - Add per-connection listeners via (*Websocket).On / (*Websocket).Off that fire alongside the process-global listeners and are discarded when the connection disconnects. Export the EventCallback type so the On signature is usable from go doc. - Add package-level Off(event) as the missing counterpart to On. - Fire EventError(ErrorInvalidConnection) when send() drops an outbound message after exhausting retries instead of dropping it silently. - De-duplicate EventError: the method EmitToList/Broadcast no longer fire a second EventError, since the EmitTo they delegate to already fires one per failed UUID. - Rewrite the README: method-vs-package send semantics, global vs per-connection listeners, SetUUID/GetUUID, thread-safety, the IsDraining upgrade gate, custom events, and EventConnect/EventDisconnect timing. Clarify the event constant godocs accordingly. - Add tests for Off, per-connection scoping, the dropped-message EventError, and the method-form EmitTo/EmitToList EventError firing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary by CodeRabbit
New Features
Documentation
Deprecated
Bug Fixes
Tests