Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion packages/loro-websocket/src/client/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
type JoinError,
} from "loro-protocol";
import * as protocol from "loro-protocol";
import { LoroWebsocketClient } from "./index";
import { JoinFailedError, LoroWebsocketClient } from "./index";

class FakeWebSocket {
static CONNECTING = 0;
Expand Down Expand Up @@ -139,4 +139,53 @@ describe("LoroWebsocketClient", () => {
expect(onError).toHaveBeenCalledTimes(1);

});

it("rejects a join with a typed error carrying the code and appCode", async () => {
const client = new LoroWebsocketClient({
url: "ws://test",
disablePing: true,
reconnect: { enabled: false },
});

(client as any).connectedPromise?.catch(() => { });

const adaptor = {
crdtType: CrdtType.Loro,
setCtx: () => { },
getVersion: () => new Uint8Array([0]),
handleJoinOk: async () => { },
waitForReachingServerVersion: async () => { },
destroy: () => { },
} satisfies any;

const joinError: JoinError = {
type: MessageType.JoinError,
code: JoinErrorCode.AppError,
message: "room was purged",
appCode: "room_purged",
crdt: adaptor.crdtType,
roomId: "room",
};

let rejected: unknown;
const pending = {
room: Promise.resolve({} as any),
resolve: () => { },
reject: (e: Error) => {
rejected = e;
},
adaptor,
roomId: "room",
} satisfies any;

await (client as any).handleJoinError(joinError, pending, adaptor.crdtType + "room");

expect(rejected).toBeInstanceOf(JoinFailedError);
expect(rejected).toMatchObject({
code: JoinErrorCode.AppError,
appCode: "room_purged",
roomId: "room",
crdt: CrdtType.Loro,
});
});
});
24 changes: 23 additions & 1 deletion packages/loro-websocket/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,28 @@ import type { CrdtDocAdaptor } from "loro-adaptors";

export * from "loro-adaptors";

/**
* Rejection reason for a failed `join`. Carries the wire fields of the
* `JoinError` that caused it so callers can branch on `code` — and, for
* `app_error`, on the application-defined `appCode` — instead of parsing
* `message`. The message text is unchanged from earlier releases.
*/
export class JoinFailedError extends Error {
readonly crdt: CrdtType;
readonly roomId: string;
readonly code: JoinErrorCode;
readonly appCode?: string;

constructor(msg: JoinError) {
super(`Join failed: ${msg.code} - ${msg.message}`);
this.name = "JoinFailedError";
this.crdt = msg.crdt;
this.roomId = msg.roomId;
this.code = msg.code;
this.appCode = msg.appCode;
}
}

export type AuthProvider = () => Uint8Array | Promise<Uint8Array>;
type AuthOption = Uint8Array | AuthProvider;

Expand Down Expand Up @@ -894,7 +916,7 @@ export class LoroWebsocketClient {
}

// No retry possible, reject the promise
const err = new Error(`Join failed: ${msg.code} - ${msg.message}`);
const err = new JoinFailedError(msg);
this.emitRoomStatus(
pending.adaptor.crdtType + pending.roomId,
RoomJoinStatus.Error,
Expand Down