UNPKG

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
'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