diff --git a/biome.json b/biome.json index 99d3350..022df3e 100644 --- a/biome.json +++ b/biome.json @@ -1,21 +1,17 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "$schema": "https://biomejs.dev/schemas/2.0.6/schema.json", "vcs": { "enabled": false, "clientKind": "git", - "useIgnoreFile": true + "useIgnoreFile": false }, "files": { "ignoreUnknown": false, - "ignore": ["dist", ".next", "public", "*.d.ts", "*.json"] + "includes": ["src/**/*.ts"] }, "formatter": { "enabled": true, - "indentStyle": "space", - "indentWidth": 2 - }, - "organizeImports": { - "enabled": true + "indentStyle": "space" }, "linter": { "enabled": true, @@ -27,5 +23,13 @@ "formatter": { "quoteStyle": "double" } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } } } diff --git a/bun.lockb b/bun.lockb index ff63894..4991ae7 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index e3f49bc..c057b76 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "@endeavorance/socket", "description": "Self-managed reconnecting WebSocket wrapper", - "version": "0.1.0", + "version": "0.2.0", "exports": "./dist/index.js", "types": "./dist/index.d.ts", "scripts": { - "build": "bun run ./build.ts" + "build": "bun run ./build.ts", + "fmt": "bunx --bun biome check --fix" }, "keywords": [], "author": "Endeavorance (https://endeavorance.camp)", @@ -15,7 +16,7 @@ ], "type": "module", "devDependencies": { - "@biomejs/biome": "^1.9.4", + "@biomejs/biome": "^2.0.6", "@types/bun": "latest", "bun-plugin-dts": "^0.2.3" }, diff --git a/src/errors.ts b/src/errors.ts index 50c4bc7..a78657f 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -7,3 +7,5 @@ export class SocketError extends Error { this.originalError = originalError; } } + +export class UnpackError extends SocketError {} diff --git a/src/index.ts b/src/index.ts index 2381a92..e3b7461 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,13 @@ -import { SocketError } from "./errors"; +import { SocketError, UnpackError } from "./errors"; +import { type DataMessage, Heartbeat, pack, unpack } from "./messages"; import { ManagedSocket } from "./socket"; -import { wrap, unwrap, deserialize } from "./messages"; -export { ManagedSocket, SocketError, wrap, unwrap, deserialize }; +export { + ManagedSocket, + SocketError, + UnpackError, + pack, + unpack, + type DataMessage, + Heartbeat, +}; diff --git a/src/messages.ts b/src/messages.ts index 646aef5..21429f9 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -1,61 +1,48 @@ -import { SocketError } from "./errors"; +import { UnpackError } from "./errors"; -export type SocketHeartbeatMessage = ["heartbeat", number]; -export type SocketDataMessage = ["data", T]; +export const Heartbeat = Symbol("Heartbeat"); -/** - * Creates a socket data message object with the given data. - * - * @param data - The data to be included in the message. Can be of any type. - * @returns An object representing the socket data message with a type of "message" and the provided data. - */ -function wrap(data: unknown): SocketDataMessage { - return ["data", data]; +export interface DataMessage { + _: unknown; +} + +export function pack(data: unknown): string { + return JSON.stringify({ _: data }) as string; } /** - * Extracts the data from a SocketDataMessage. - * - * @param message - The socket data message to extract data from. - * @returns The data contained in the message. - */ -function unwrap(message: SocketDataMessage): T { - return message[1]; -} - -/** - * Deserializes a JSON string into a `SocketMessage` object. + * Returns the data in the message or the Heartbeat symbol * * @param data - The JSON string to be deserialized. - * @returns The deserialized `SocketMessage` object. + * @returns The deserialized data or the Heartbeat symbol * @throws {ManagedSocketError} If the JSON string cannot be parsed or if the message shape is invalid. */ -function deserialize( - data: string, -): SocketDataMessage | SocketHeartbeatMessage { +export function unpack(data: string): { data: unknown } | typeof Heartbeat { try { const parsedData = JSON.parse(data); + // If the parsed data is a number, it's a heartbeat if (typeof parsedData === "number") { - return ["heartbeat", parsedData]; + return Heartbeat; } + // Otherwise, the shape should be an object with a _ property holding the data if ( + typeof parsedData === "object" && parsedData !== null && - Array.isArray(parsedData) && - parsedData.length === 2 && - parsedData[0] === "data" + "_" in parsedData && + typeof parsedData._ !== "undefined" ) { - const data = parsedData[1] as T; - return ["data", data]; + return { data: parsedData._ }; } - throw new SocketError("Failed to parse internal message shape"); + throw new UnpackError("Unpack Error: Invalid message shape"); } catch (error) { - throw new SocketError( - "Failed to parse incoming message shape: Failed to deserialize", - ); + if (error instanceof Error) { + throw new UnpackError("Unpack Error: Failed to deserialize", error); + } + + // Raise unknown exceptions + throw error; } } - -export { wrap, unwrap, deserialize }; diff --git a/src/socket.ts b/src/socket.ts index 96b3ba0..3d41c0b 100644 --- a/src/socket.ts +++ b/src/socket.ts @@ -1,9 +1,7 @@ import { SocketError } from "./errors"; -import { deserialize, wrap } from "./messages"; +import { pack, unpack } from "./messages"; -const ONE_SECOND = 1000; -const HEARTBEAT_INTERVAL = 30 * ONE_SECOND; -const MAX_RECONNECT_DELAY = 30 * ONE_SECOND; +const THIRTY_SECONDS = 30 * 1000; type SocketDataHandler = (data: unknown) => void | Promise; type SocketErrorHandler = (error: SocketError) => void; @@ -50,8 +48,8 @@ export class ManagedSocket { this.socket = this.connect(); setInterval(() => { - this.heartbeat(); - }, HEARTBEAT_INTERVAL); + this.sendHeartbeat(); + }, THIRTY_SECONDS); window.addEventListener("offline", () => { this._offline = true; @@ -74,7 +72,7 @@ export class ManagedSocket { }); } - private heartbeat() { + private sendHeartbeat() { if (this.socket?.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify(Date.now())); } @@ -104,10 +102,7 @@ export class ManagedSocket { return; } - const delay = Math.min( - ONE_SECOND * 2 ** this._reconnectAttempts, - MAX_RECONNECT_DELAY, - ); + const delay = Math.min(2000 ** this._reconnectAttempts, THIRTY_SECONDS); this.debugMessage(`Attempting to reconnect in ${delay}ms`); setTimeout(() => { this._reconnectAttempts++; @@ -159,9 +154,9 @@ export class ManagedSocket { }); newSocket.addEventListener("message", (messageEvent) => { - const [type, data] = deserialize(messageEvent.data); + const data = unpack(messageEvent.data); - if (type === "heartbeat") { + if (data === null) { return; } @@ -189,13 +184,13 @@ export class ManagedSocket { this.socket.close(); } - send(data: T) { + send(data: unknown) { if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { this.debugMessage("WebSocket is not open. Queuing message."); this._messageQueue.push(data); return; } - this.socket.send(JSON.stringify(wrap(data))); + this.socket.send(pack(data)); } } diff --git a/tsconfig.json b/tsconfig.json index 9195094..d104931 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,5 +25,5 @@ "noPropertyAccessFromIndexSignature": false }, "include": ["src"], - "exclude": ["src/test"], + "exclude": ["src/test"] }