plug-n-play-ws
Version:
A plug-and-play WebSocket layer on top of Socket.IO with full TypeScript support, zero manual wiring, and production-ready features
296 lines (292 loc) • 8.76 kB
JavaScript
'use strict';
var socket_ioClient = require('socket.io-client');
var eventemitter3 = require('eventemitter3');
var zod = require('zod');
// src/client/index.ts
var ConsoleLogger = class {
debug(message, meta) {
console.debug(`[DEBUG] ${message}`, meta || "");
}
info(message, meta) {
console.info(`[INFO] ${message}`, meta || "");
}
warn(message, meta) {
console.warn(`[WARN] ${message}`, meta || "");
}
error(message, meta) {
console.error(`[ERROR] ${message}`, meta || "");
}
};
zod.z.object({
id: zod.z.string(),
userId: zod.z.string().optional(),
tabId: zod.z.string().optional(),
userAgent: zod.z.string().optional(),
ip: zod.z.string().optional(),
connectedAt: zod.z.date(),
lastSeenAt: zod.z.date(),
metadata: zod.z.record(zod.z.unknown()).optional()
});
var SearchQuerySchema = zod.z.object({
query: zod.z.string().min(1),
limit: zod.z.number().int().positive().max(1e3).default(10),
offset: zod.z.number().int().min(0).default(0),
filters: zod.z.record(zod.z.unknown()).optional(),
streaming: zod.z.boolean().default(false)
});
var SearchResultSchema = zod.z.object({
id: zod.z.string(),
score: zod.z.number(),
data: zod.z.unknown(),
highlights: zod.z.array(zod.z.string()).optional()
});
zod.z.object({
query: zod.z.string(),
results: zod.z.array(SearchResultSchema),
total: zod.z.number().int().min(0),
took: zod.z.number().min(0),
hasMore: zod.z.boolean().optional()
});
// src/client/index.ts
var PlugNPlayClient = class {
constructor(config) {
this.config = config;
this.emitter = new eventemitter3.EventEmitter();
this.logger = config.logger || new ConsoleLogger();
if (config.autoConnect !== false) {
void this.connect();
}
}
socket;
emitter;
logger;
status = "disconnected" /* DISCONNECTED */;
sessionId;
sessionMetadata;
reconnectAttempts = 0;
heartbeatInterval;
lastPongTime = 0;
// Event emitter methods
on(event, listener) {
this.emitter.on(event, listener);
return this;
}
off(event, listener) {
this.emitter.off(event, listener);
return this;
}
emit(event, data) {
return this.emitter.emit(event, data);
}
once(event, listener) {
this.emitter.once(event, listener);
return this;
}
removeAllListeners(event) {
this.emitter.removeAllListeners(event);
return this;
}
/**
* Connect to the WebSocket server
*/
async connect() {
if (this.socket?.connected) {
return;
}
this.setStatus("connecting" /* CONNECTING */);
this.logger.info("Connecting to WebSocket server", { url: this.config.url });
return new Promise((resolve, reject) => {
this.socket = socket_ioClient.io(this.config.url, {
autoConnect: false,
reconnection: this.config.reconnection !== false,
reconnectionAttempts: this.config.reconnectionAttempts || 5,
reconnectionDelay: this.config.reconnectionDelay || 1e3,
reconnectionDelayMax: this.config.reconnectionDelayMax || 5e3,
timeout: this.config.timeout || 2e4,
forceNew: this.config.forceNew || false,
auth: this.config.auth || {}
});
this.setupSocketHandlers(resolve, reject);
this.socket.connect();
});
}
/**
* Disconnect from the WebSocket server
*/
disconnect() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
delete this.heartbeatInterval;
}
if (this.socket) {
this.socket.disconnect();
this.socket = void 0;
}
this.setStatus("disconnected" /* DISCONNECTED */);
this.reconnectAttempts = 0;
}
/**
* Completely clear session and disconnect
*/
clearSession() {
this.disconnect();
delete this.sessionId;
delete this.sessionMetadata;
}
/**
* Send a typed message to the server
*/
send(event, data) {
if (!this.socket?.connected) {
this.logger.warn("Cannot send message: not connected", { event });
return false;
}
this.socket.emit(event, data);
return true;
}
/**
* Perform a search query
*/
async search(query) {
if (!this.socket?.connected) {
this.logger.warn("Cannot search: not connected");
return null;
}
try {
const validatedQuery = SearchQuerySchema.parse(query);
return new Promise((resolve) => {
const timeout = setTimeout(() => {
this.socket?.off("search-result", handleResult);
this.socket?.off("error", handleError);
resolve(null);
}, this.config.searchTimeout ?? 3e4);
const handleResult = (result) => {
clearTimeout(timeout);
this.socket?.off("search-result", handleResult);
this.socket?.off("error", handleError);
resolve(result);
};
const handleError = (error) => {
clearTimeout(timeout);
this.socket?.off("search-result", handleResult);
this.socket?.off("error", handleError);
this.logger.error("Search failed", { error: error.error.message });
resolve(null);
};
this.socket?.once("search-result", handleResult);
this.socket?.once("error", handleError);
this.socket?.emit("search", validatedQuery);
});
} catch (error) {
this.logger.error("Invalid search query", {
error: error instanceof Error ? error.message : "Unknown validation error"
});
return null;
}
}
/**
* Get current connection status
*/
getStatus() {
return this.status;
}
/**
* Get current session information
*/
getSession() {
return {
...this.sessionId && { id: this.sessionId },
...this.sessionMetadata && { metadata: this.sessionMetadata }
};
}
/**
* Check if client is connected
*/
isConnected() {
return this.status === "connected" /* CONNECTED */ && !!this.socket?.connected;
}
/**
* Get connection statistics
*/
getStats() {
return {
status: this.status,
sessionId: this.sessionId,
reconnectAttempts: this.reconnectAttempts,
lastPongTime: this.lastPongTime,
connected: this.isConnected()
};
}
setupSocketHandlers(resolve, reject) {
if (!this.socket) return;
this.socket.on("connect", () => {
this.setStatus("connected" /* CONNECTED */);
this.reconnectAttempts = 0;
this.logger.info("Connected to WebSocket server");
this.startHeartbeat();
resolve();
});
this.socket.on("session", (data) => {
this.sessionId = data.sessionId;
this.sessionMetadata = data.metadata;
this.emit("connect", data);
});
this.socket.on("connect_error", (error) => {
this.setStatus("error" /* ERROR */);
this.logger.error("Connection error", { error: error.message });
if (this.reconnectAttempts === 0) {
reject(error);
}
});
this.socket.on("disconnect", (reason) => {
this.setStatus("disconnected" /* DISCONNECTED */);
this.logger.info("Disconnected from WebSocket server", { reason });
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
delete this.heartbeatInterval;
}
this.emit("disconnect", { sessionId: this.sessionId || "", reason });
});
this.socket.on("reconnect_attempt", (attemptNumber) => {
this.setStatus("reconnecting" /* RECONNECTING */);
this.reconnectAttempts = attemptNumber;
this.logger.info("Reconnection attempt", { attempt: attemptNumber });
});
this.socket.on("reconnect", (attemptNumber) => {
this.setStatus("connected" /* CONNECTED */);
this.logger.info("Reconnected to WebSocket server", { attempts: attemptNumber });
this.startHeartbeat();
});
this.socket.on("pong", (data) => {
this.lastPongTime = data.timestamp;
});
this.socket.on("search-stream", (data) => {
this.emit("search-stream", data);
});
this.socket.onAny((event, data) => {
if (!["connect", "connect_error", "disconnect", "reconnect_attempt", "reconnect", "pong", "search-stream"].includes(event)) {
this.emit(event, data);
}
});
}
setStatus(status) {
if (this.status !== status) {
const oldStatus = this.status;
this.status = status;
this.logger.debug("Status changed", { from: oldStatus, to: status });
}
}
startHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
this.heartbeatInterval = setInterval(() => {
if (this.socket?.connected) {
this.socket.emit("ping", { timestamp: Date.now() });
}
}, 3e4);
}
};
exports.PlugNPlayClient = PlugNPlayClient;
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map