Skip to content

Migrating to v0.1.0

Gerasimos (Makis) Maropoulos edited this page Oct 6, 2026 · 1 revision

When to use this page

  • 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, Broadcast or EnableDebug and you want the one-line fix for each.

TL;DR

go get github.com/kataras/neffos@v0.1.0

The 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 and the API changes

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(&notes{}).
	SetNamespace("notes").
	SetInjector(func(c *neffos.NSConn) *notes {
		return &notes{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.

New

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"}

See Close codes and shutdown.

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)

See Close codes and shutdown.

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)

See Upgraders and Dialers.

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")

See Scale out using Redis.

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.

Fixed

These were bugs in v0.0.x; nothing about calling the library changes, the wrong behavior is just gone:

  • Ask could hang forever. Calling Ask from 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: Ask always returns once its context is done or the connection closes, inside a handler or not.
  • Rare deadlocks in DisconnectAll, LeaveAll and Close. A callback that read the connection's own namespace or room list from inside OnNamespaceDisconnect/OnRoomLeave while 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 Ask calls 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/Decrement race. 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.Unmarshal on a nil pointer, calling Broadcast/BroadcastOthers on a client-side NSConn, and setting both a wait token and FromExplicit on the same message. All of these now return a sane result or a no-op instead of crashing. (EnableDebug with an unsupported printer is a compile error now, see the table above.)
  • A dynamic Struct with an unexported field panicked on the first connection (reflect.Value.Set on the field it could not set). Unexported fields are now skipped when the prototype's values are copied; set them from SetInjector if 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.Close closed 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.Broadcast calls 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.To targets stopped at the first message that did not match a given connection, so later messages in the same Broadcast(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 per Ask or per websocket connection, and OnConnect/Subscribe are now bounded by the connect timeout instead of hanging against a broker that never answers.

Behaviour changes to check

Four things that are not bugs, but can be visible if your code depends on the old behaviour:

  1. A close frame is actually sent now, where it previously may not have been. Server.Close/Shutdown send a real CloseGoingAway (1001) frame to every connection instead of just dropping the socket. A client checking CloseStatus(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; -1 means no code at all, from a timeout, the heartbeat, or a hard TCP reset.)
  2. Server.OnDisconnect now reliably fires once per connection before Server.Close returns. If your bookkeeping assumed Close could return before every disconnect notification had run, it no longer can; Close waits for all of them (concurrently, not in order).
  3. An Ask made 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). An Ask made 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-goroutine Ask landing 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. See Conn.Ask's doc comment for the precise conditions.
  4. JoinConnHandlers can now return a WithTimeout instead of a Namespaces. If any of the joined handlers carries timeouts, a heartbeat interval or a size limit, the combined result is a WithTimeout so those settings survive the join. Code that type-asserted the result of JoinConnHandlers specifically to Namespaces will see a different type in that case; asserting to neffos.ConnHandler (what New/Dial actually want) is unaffected.

neffos.js 0.3.0 pairing

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) and neffos.cjs (CommonJS) for import * as neffos from "neffos.js" / require("neffos.js"), plus neffos.browser.js and the neffos.global.js/neffos.global.min.js IIFE for a <script> tag.
  • ws is optional. It is only reached through a dynamic import("ws"), used on Node below 22 (no global WebSocket) or when you pass a custom Options.WebSocket built on it to get real handshake headers. Browsers and Node 22+ need nothing extra; headers go as X-Websocket-Header-... URL parameters instead of real headers in that case.
  • Breaking changes, the ones most likely to affect existing code:
    • A binary Message.Body is a Uint8Array, not an ArrayBuffer; use the new msg.bytes()/msg.text() helpers instead of checking the type yourself.
    • Errors are NeffosError/CloseError instances with a code, not plain strings or bare Errors; isCloseError(err) replaces string matching.
    • Room.leave(), NSConn.leaveAll() and NSConn.disconnect() return a Promise<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 a TypeError instead of rejecting with a string.
    • Reconnect is a full rewrite: exponential backoff with jitter by default (reconnect: 5000 now means "first retry after 5s, then back off"), the same Conn is reused across a reconnect, and the old always-on HTTP HEAD probe is now opt-in (probe: true). See Reconnection.
    • ask(event, body, options?) takes an AskOptions object (timeout, signal) instead of nothing; handlers may now be async and return a Promise.

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.

Examples moved

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.

See also

Clone this wiki locally