0.2.0
This commit is contained in:
parent
466fa26cfd
commit
b6b4ceb4ae
8 changed files with 65 additions and 68 deletions
20
biome.json
20
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -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 <hello@endeavorance.camp> (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"
|
||||
},
|
||||
|
|
|
@ -7,3 +7,5 @@ export class SocketError extends Error {
|
|||
this.originalError = originalError;
|
||||
}
|
||||
}
|
||||
|
||||
export class UnpackError extends SocketError {}
|
||||
|
|
14
src/index.ts
14
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,
|
||||
};
|
||||
|
|
|
@ -1,61 +1,48 @@
|
|||
import { SocketError } from "./errors";
|
||||
import { UnpackError } from "./errors";
|
||||
|
||||
export type SocketHeartbeatMessage = ["heartbeat", number];
|
||||
export type SocketDataMessage<T = unknown> = ["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<T = unknown>(message: SocketDataMessage<T>): 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<T>(
|
||||
data: string,
|
||||
): SocketDataMessage<T> | 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 };
|
||||
|
|
|
@ -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<void>;
|
||||
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<T>(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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,5 +25,5 @@
|
|||
"noPropertyAccessFromIndexSignature": false
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/test"],
|
||||
"exclude": ["src/test"]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue