Skip to content

feat(loro-websocket): surface JoinError code and appCode on the join rejection - #64

Open
jibize wants to merge 1 commit into
loro-dev:mainfrom
jibize:feat-room-purged-code
Open

feat(loro-websocket): surface JoinError code and appCode on the join rejection#64
jibize wants to merge 1 commit into
loro-dev:mainfrom
jibize:feat-room-purged-code

Conversation

@jibize

@jibize jibize commented Aug 26, 2026

Copy link
Copy Markdown

Motivation

When a join is refused, LoroWebsocketClient rejects with a plain Error whose only content is the formatted string:

const err = new Error(`Join failed: ${msg.code} - ${msg.message}`);

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:

Implementations may expose onError({ roomId, kind, code, message, app_code? }) for join or room messages …

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:

  • Add an exported JoinFailedError extends Error carrying code, appCode, roomId and crdt.
  • Reject from handleJoinError with it instead of a bare Error.

Notes:

  • No wire change. No new enum member, no renumbering, no encoder/decoder edit — appCode was already on the wire and already decoded. Nothing in rust/ needs a counterpart.
  • Backwards compatible. err.message is byte-identical to before (Join failed: <code> - <message>), so consumers still matching on the string are unaffected; JoinFailedError is an Error, so instanceof Error and existing catch blocks behave the same.
  • The VersionUnknown retry path is untouched.

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.

…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? })`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant