Repository navigation
Migrating to v0.1.0
- You're upgrading a project from neffos v0.0.x and want the full list of what changed, beyond the short summary on Installation.
- You're upgrading neffos.js from 0.2.0 to 0.3.0 alongside the Go server.
- You moved an old
_examples/path in a fork or a bookmark and need the new one. - Your build broke on
neffos.Marshal,Conn.Get,NewStruct,SetInjector,BroadcastorEnableDebugand you want the one-line fix for each.
go get github.com/kataras/neffos@v0.1.0The module floor is Go 1.27. Most of v0.1.0 is additive, but a handful of signatures change where the old form took an any or exposed reflect; every one of them is a compile error with a one-line fix, listed in the table under "Go 1.27 and the API changes" right below. Four runtime differences are called out under "Behaviour changes to check". Pair it with neffos.js 0.3.0 if you have a JavaScript client; that one does have breaking changes, listed near the bottom of this page. The wire protocol is unchanged, so old and new servers and clients keep talking to each other during a rollout.
Go 1.27 lets a method declare its own type parameters, and neffos uses that where v0.0.x returned any or asked for a type assertion. The compiler finds every call site for you; this table is the fix for each.
| v0.0.x | v0.1.0 |
|---|---|
neffos.Marshal(v) []byte |
neffos.Marshal(v) ([]byte, error); usually you want c.SendObject(event, v), room.SendObject(event, v), c.AskObject[Reply](ctx, event, v) or neffos.ReplyObject(v) instead |
var v T; msg.Unmarshal(&v) |
still works; or v, err := msg.As[T]()
|
neffos.DefaultMarshaler = json.Marshal (and DefaultUnmarshaler) |
typed neffos.Marshaler / neffos.Unmarshaler on encoding/json/v2; tune them through neffos.JSONMarshalOptions / neffos.JSONUnmarshalOptions
|
c.Conn.Get("user").(User) |
user, ok := c.Conn.Value[User]("user") |
neffos.NewStruct(&ctrl{}) *Struct |
same call, returns *neffos.Struct[ctrl]
|
neffos.NewStruct(reflectValue) |
neffos.NewStructValue(reflectValue) *neffos.StructValue |
SetInjector(func(reflect.Type, *NSConn) reflect.Value) |
SetInjector(func(*neffos.NSConn) *ctrl); neffos.StructInjector is gone |
Server.Broadcast(exceptSender fmt.Stringer, ...), Exclude(id) fmt.Stringer
|
Server.Broadcast(neffos.Sender, ...), Exclude(id) neffos.Sender; passing a *Conn, *NSConn, *Room or Exclude(id) compiles as before |
neffos.EnableDebug(printer any) |
neffos.EnableDebug(neffos.Printer); *log.Logger qualifies, wrap other loggers in neffos.PrinterFunc
|
neffos.DebugEach |
removed; it was internal plumbing |
var neffos.EventPrefixMatcher, var neffos.EventTrimPrefixMatcher
|
plain functions, same call syntax |
github.com/google/uuid in go.sum
|
gone; the standard library's uuid package generates the ids, still UUID v4 strings |
Typed message helpers. The common handler shape loses its boilerplate: the request decodes into a value, the reply encodes from one, and an encoding failure is an error instead of a body that reads json: unsupported type.
"login": func(c *neffos.NSConn, msg neffos.Message) error {
req, err := msg.As[LoginRequest]()
if err != nil {
return err
}
user, err := users.Login(req)
if err != nil {
return err
}
c.Conn.Set("user", user)
return neffos.ReplyObject(user)
},// client side, a typed round trip:
user, err := c.AskObject[User](ctx, "login", LoginRequest{Name: "makis"})See Encoding and The ask method.
Typed per-connection storage. Conn.Value[T] replaces Conn.Get and the type assertion that always followed it; ok is false when the key is missing or holds another type.
user, ok := c.Conn.Value[User]("user")See Connections.
Typed struct handlers. NewStruct(&ctrl{}) returns a *Struct[ctrl], so the injector that builds each connection's controller is func(*neffos.NSConn) *ctrl and no longer touches reflect. c.Instance[ctrl]() reads a connection's controller from code outside its methods. Frameworks that only hold a reflect.Value (iris's mvc websocket controllers) use NewStructValue.
controller := neffos.NewStruct(¬es{}).
SetNamespace("notes").
SetInjector(func(c *neffos.NSConn) *notes {
return ¬es{store: store, audit: log}
})See Struct handlers.
encoding/json/v2 bodies. Message bodies are encoded with the standard library's new JSON package. neffos keeps the v1 behaviours a deployed client depends on (sorted map keys, time.Duration as nanoseconds, case-insensitive field matching on decode, invalid UTF-8 replaced rather than rejected on encode) and takes the v2 ones that only the Go side sees. Check your own structs for: a nil slice now encodes as [], not null; omitempty only drops empty JSON values, so a 0, false or zero time.Duration that used to disappear is now sent (use omitzero for the old effect); <, > and & are not HTML-escaped; an incoming body with a duplicated key or invalid UTF-8 is rejected with an error. To restore a v1 behaviour, extend the options:
neffos.JSONMarshalOptions = json.JoinOptions(neffos.JSONMarshalOptions, json.FormatNilSliceAsNull(true))See Encoding.
Exclude and *Room work through a StackExchange. Both used to be silently ignored whenever redis or nats carried the broadcast; they now travel like a *Conn does. See Broadcast.
Close codes. Return a neffos.CloseError{Code, Reason} from any handler, or call Conn.Terminate(code, reason), to close with a specific WebSocket close code instead of a plain drop.
return neffos.CloseError{Code: 4003, Reason: "banned"}Heartbeat. WithTimeout.PingInterval (or Struct.SetPingInterval) pings the remote side and closes the connection if no pong arrives in time.
neffos.WithTimeout{PingInterval: 30 * time.Second, Namespaces: ns}See Timeouts.
Message size limit. WithTimeout.MaxMessageSize (or Struct.SetMaxMessageSize) closes an oversized incoming message with code 1009 instead of growing memory without bound.
neffos.WithTimeout{MaxMessageSize: 1 << 20, Namespaces: ns}See Timeouts.
Send, the error-returning write. Conn.Send, NSConn.Send and Room.Send do the same write as Write/Emit but return an error you can branch on (ErrBadNamespace, ErrBadRoom, a closed connection) instead of just a bool.
err := nsConn.Send("chat", body)
// errors.Is(err, neffos.ErrBadNamespace) etc.See Broadcast.
Server.Shutdown. Closes the server like Close, then waits for every connection's reader goroutine, including any event callback still running, up to a deadline.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
err := srv.Shutdown(ctx)CloseError{Code: 1008} is safe to use now. The literal already compiled in v0.0.x, there was no constructor to go through, but calling .Error() on one built this way panicked: the old Error() always called the embedded, nil, underlying error's .Error() method. Error() is nil-safe now (it falls back to the public Reason field), and Unwrap() exposes any underlying error to errors.Is/errors.As.
err := neffos.CloseError{Code: 4000, Reason: "kicked"}Typed message types. neffos.MessageType (TextMessage, BinaryMessage) has a String() method, for code that logs or branches on which frame kind arrived.
frame := neffos.TextMessage
if msg.SetBinary { frame = neffos.BinaryMessage }See Binary messages.
Client.Conn(). Returns the client-side *Conn, so you can call Conn.Err() or Conn.Terminate without threading it through separately.
err := client.Conn().Err()
code := neffos.CloseStatus(err)The coder backend. A third adapter, github.com/kataras/neffos/coder, alongside gorilla and gobwas, built on github.com/coder/websocket.
server := neffos.New(coder.DefaultUpgrader, handler)
client, err := neffos.Dial(ctx, coder.DefaultDialer, url, handler)Redis on go-redis v9. The redis StackExchange moved off radix onto github.com/redis/go-redis/v9; the exported redis.Config and every method keep their old names and signatures.
exc, err := redis.NewStackExchange(redis.Config{Addr: "127.0.0.1:6379"}, "neffos-app")Nats fixes. The nats StackExchange no longer opens a connection per websocket connection or a goroutine per Ask; it now runs on two nats connections total for the whole exchange. The exported API is unchanged.
exc, err := nats.NewStackExchange("nats://127.0.0.1:4222")See Scale out using Nats.
These were bugs in v0.0.x; nothing about calling the library changes, the wrong behavior is just gone:
-
Askcould hang forever. CallingAskfrom inside an event callback could deadlock the connection's reader goroutine; a cancelled context racing with a reply could also stall it. Both are fixed:Askalways returns once its context is done or the connection closes, inside a handler or not. -
Rare deadlocks in
DisconnectAll,LeaveAllandClose. A callback that read the connection's own namespace or room list from insideOnNamespaceDisconnect/OnRoomLeavewhile one of these was running could deadlock on the same lock. Fixed by taking a snapshot of the names first and firing callbacks with no lock held. -
Wait-token collisions under load. Two
Askcalls landing in the same nanosecond could get the same wait token and cross-deliver their replies. Tokens are now sequence-numbered and unique. -
Conn.Increment/Decrementrace. Concurrent calls on the same key could lose updates. Both now happen under one lock per call. -
A few panics on edge-case input:
neffos.Marshal/msg.Unmarshalon anilpointer, callingBroadcast/BroadcastOtherson a client-sideNSConn, and setting both a wait token andFromExpliciton the same message. All of these now return a sane result or a no-op instead of crashing. (EnableDebugwith an unsupported printer is a compile error now, see the table above.) -
A dynamic
Structwith an unexported field panicked on the first connection (reflect.Value.Seton the field it could not set). Unexported fields are now skipped when the prototype's values are copied; set them fromSetInjectorif they need a value. - gobwas: a stray pong frame could land in the middle of a data frame, corrupting it. Control replies are now serialized with data writes.
- gobwas: data buffered in the same TCP segment as the handshake could be lost, on both the upgrader and the dialer side. Fixed on both.
-
Server.Closeclosed connections one at a time, so a slow or unresponsive peer delayed every connection after it in the list. Connections now close concurrently. -
The async broadcaster could drop messages for a connection that was still writing a previous one: back-to-back
Server.Broadcastcalls could lose all but the first for a slow connection. It now delivers every message to every connection, in order, however far behind the connection's writer is; the backlog queues in memory until the connection catches up or its write deadline or the heartbeat closes it (see Broadcast). -
A broadcast batch with mixed
Message.Totargets stopped at the first message that did not match a given connection, so later messages in the sameBroadcast(nil, msgA, msgB)call could silently never reach their own target. Fixed; each message in a batch is now evaluated independently. -
The redis and nats
StackExchanges no longer panic on an empty namespace, no longer leak a goroutine or a connection perAskor per websocket connection, andOnConnect/Subscribeare now bounded by the connect timeout instead of hanging against a broker that never answers.
Four things that are not bugs, but can be visible if your code depends on the old behaviour:
-
A close frame is actually sent now, where it previously may not have been.
Server.Close/Shutdownsend a realCloseGoingAway(1001) frame to every connection instead of just dropping the socket. A client checkingCloseStatus(c.Err())after a server shutdown now reliably sees 1001. (A peer that drops with a clean FIN and no close frame at all still reports 1006,CloseAbnormalClosure, on all three backends;-1means no code at all, from a timeout, the heartbeat, or a hard TCP reset.) -
Server.OnDisconnectnow reliably fires once per connection beforeServer.Closereturns. If your bookkeeping assumedClosecould return before every disconnect notification had run, it no longer can;Closewaits for all of them (concurrently, not in order). -
An
Askmade from inside a handler now dispatches unrelated incoming frames to their own events while it waits, on the same goroutine, so handlers can run re-entrantly (see The ask method). AnAskmade from outside a handler, while that connection's reader goroutine is busy in another handler, can additionally run its own event callbacks on its own goroutine, concurrently with the reader goroutine and out of order with the next frame the reader reads; that only happens with that specific interleaving (an outside-goroutineAsklanding mid-handler). A handler that assumed every callback on a connection runs strictly on one goroutine, one at a time, should know about both cases. SeeConn.Ask's doc comment for the precise conditions. -
JoinConnHandlerscan now return aWithTimeoutinstead of aNamespaces. If any of the joined handlers carries timeouts, a heartbeat interval or a size limit, the combined result is aWithTimeoutso those settings survive the join. Code that type-asserted the result ofJoinConnHandlersspecifically toNamespaceswill see a different type in that case; asserting toneffos.ConnHandler(whatNew/Dialactually want) is unaffected.
Pair v0.1.0 with neffos.js 0.3.0: both speak the same wire protocol as the v0.0.x line, so you can upgrade either side first, but 0.3.0 is where the JavaScript client's own breaking changes land.
-
Bundle names:
neffos.js(ESM) andneffos.cjs(CommonJS) forimport * as neffos from "neffos.js"/require("neffos.js"), plusneffos.browser.jsand theneffos.global.js/neffos.global.min.jsIIFE for a<script>tag. -
wsis optional. It is only reached through a dynamicimport("ws"), used on Node below 22 (no globalWebSocket) or when you pass a customOptions.WebSocketbuilt on it to get real handshake headers. Browsers and Node 22+ need nothing extra; headers go asX-Websocket-Header-...URL parameters instead of real headers in that case. -
Breaking changes, the ones most likely to affect existing code:
- A binary
Message.Bodyis aUint8Array, not anArrayBuffer; use the newmsg.bytes()/msg.text()helpers instead of checking the type yourself. - Errors are
NeffosError/CloseErrorinstances with acode, not plain strings or bareErrors;isCloseError(err)replaces string matching. -
Room.leave(),NSConn.leaveAll()andNSConn.disconnect()return aPromise<void>that rejects on error; they used to resolve even on failure. -
dial(endpoint, connHandler, options?)is the only signature now; the old second overload is gone, and a bad handler map throws aTypeErrorinstead of rejecting with a string. - Reconnect is a full rewrite: exponential backoff with jitter by default (
reconnect: 5000now means "first retry after 5s, then back off"), the sameConnis reused across a reconnect, and the old always-on HTTPHEADprobe is now opt-in (probe: true). See Reconnection. -
ask(event, body, options?)takes anAskOptionsobject (timeout,signal) instead of nothing; handlers may now beasyncand return aPromise.
- A binary
See the neffos.js repository's own HISTORY.md for the complete breaking-change list once 0.3.0 is published; this page carries the items most likely to show up in an existing app.
The _examples/ tree was reorganized into numbered topic folders. If you had a bookmark or a fork pointing at an old path:
| Old path | New path |
|---|---|
_examples/basic/, _examples/example/
|
folded into _examples/01-getting-started/02-namespaces through 05-encoding
|
_examples/native-messages/, _examples/browser/
|
_examples/03-messaging/native-messages/ |
_examples/protobuf/ |
_examples/03-messaging/protobuf/ |
_examples/cronjob/ |
_examples/06-server-push/cron-notifications/ |
_examples/scale-out/ |
_examples/07-scale-out/redis-or-nats/ |
_examples/wrap-conns-to-user/ |
_examples/05-connections/users-and-devices/ |
_examples/struct-handler/ |
folded into _examples/01-getting-started/10-struct-handler
|
_examples/stress-test/server |
_examples/08-load-testing/server |
_examples/stress-test/client (+ test.data) |
_examples/08-load-testing/clients |
_examples/stress-test/broadcasting-1 |
_examples/08-load-testing/broadcast |
Every example is now a single main.go (plus an index.html where a browser page helps), no per-example go.mod, no bundled JavaScript. See Examples for the full current catalogue.
-
Installation, the version floor and how to pin
v0.1.0 - Encoding, Connections, Struct handlers, Broadcast, Debugging, the pages behind each row of the API table
- Close codes and shutdown, Timeouts, Binary messages, the new APIs in full
- Scale out using Redis, Scale out using Nats, the two fixed backends
- Examples, the reorganized example catalogue
Home | About | Project | Getting Started | Technical Docs | Copyright © 2019-2026 Gerasimos Maropoulos. Documentation terms of use.
Getting started
Concepts
Messaging
Production
Scale out