…rejection
`handleJoinError` rejected with a plain `Error` whose only content was the
formatted string `Join failed: <code> - <message>`, so an application had to
parse that text to tell one refusal from another. The `app_error` (0x7F)
code exists precisely so a server can attach an application-defined
`app_code` (protocol.md: "Extra `varString app_code` (free-form, e.g.,
`quota_exceeded`)"), and the decoder already reads it — but the client
dropped it before the value reached the caller.
Reject with a `JoinFailedError` carrying `code`, `appCode`, `roomId` and
`crdt`. The `message` text is byte-identical to before, so consumers that
still match on the string keep working.
This is the client-side half of the optional hook protocol.md already
describes: `onError({ roomId, kind, code, message, app_code? })`.
Motivation
When a join is refused, LoroWebsocketClient rejects with a plain Error whose only content is the formatted string:
That leaves an application no way to tell one refusal from another except by parsing that text. The protocol already anticipates this case — protocol.md documents 0x7F app_error as carrying an extra varString app_code ("free-form, e.g. quota_exceeded"), and encoding.ts decodes it faithfully — but the client discards appCode (and the numeric code) before the value reaches the caller. protocol.md also sketches the hook this closes:
Our own use case: our server permanently destroys a room when the underlying record is purged, and refuses any later join for it. Clients must treat that refusal as terminal — evict the local document, stop the reconnect loop — while every other refusal stays retryable. Today we express that by sending app_error with a sentinel message string and substring-matching it on the client, with a comment next to the matcher saying "switch to err.code when the library exposes it". With this change we can send it as an app_code and branch on a typed field instead.
Scope
One narrow change, in packages/loro-websocket:
Notes:
I deliberately left the onRoomStatusChange listener signature alone — it already forwards the numeric code as messageCode, and widening it to carry appCode felt like a separate call. Happy to add it here if you'd prefer the two paths to match.
Tests
pnpm -r test — one test added in packages/loro-websocket/src/client/index.test.ts asserting an app_error join refusal rejects with a JoinFailedError exposing code and appCode. Full suite green (104 passing).
pnpm lint is clean (3 pre-existing warnings, 0 errors) and packages/loro-websocket typechecks. Two notes on pre-existing state, unrelated to this branch: pnpm typecheck fails in loro-adaptors (src/elo-adaptor.ts, duplicate CryptoKey types), and pnpm format:check is red for 34 files on main — including both files this PR touches — so I matched the surrounding style rather than reformatting them.
Thanks for the protocol and for the work on this repo — happy to adjust the shape of this however you'd like.