{"version":3,"sources":["/home/pitok/Projects/framework-bun/packages/buntok-core/dist/ws-helpers.cjs","../src/ws-helpers.ts"],"names":[],"mappings":"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","file":"/home/pitok/Projects/framework-bun/packages/buntok-core/dist/ws-helpers.cjs","sourcesContent":[null,"import type { ServerWebSocket } from \"bun\";\nimport { z } from \"zod\";\nimport type { WSData, WSHandler } from \"./app\";\n\n// Extended WSData with heartbeat\ninterface WSDataWithHeartbeat<DI> extends WSData<DI> {\n\theartbeat?: {\n\t\tinterval: number;\n\t\ttimer: ReturnType<typeof setInterval> | null;\n\t\talive: boolean;\n\t};\n}\n\n// ── Pluggable PubSub (mirip StorageDriver) ──\nexport interface WSPubSub {\n\t/** Publish ke channel - memory default, Redis pluggable */\n\tpublish(channel: string, message: string | object): Promise<void> | void;\n\t/** Subscribe - return unsubscribe */\n\tsubscribe(channel: string, handler: (message: string | object) => void): () => void;\n}\n\nexport class MemoryWSPubSub implements WSPubSub {\n\tprivate channels = new Map<string, Set<(m: string | object) => void>>();\n\tpublish(channel: string, message: string | object): void {\n\t\tconst subs = this.channels.get(channel);\n\t\tif (!subs) return;\n\t\tfor (const cb of subs) {\n\t\t\ttry { cb(message); } catch {}\n\t\t}\n\t}\n\tsubscribe(channel: string, handler: (m: string | object) => void): () => void {\n\t\tlet set = this.channels.get(channel);\n\t\tif (!set) { set = new Set(); this.channels.set(channel, set); }\n\t\tset.add(handler);\n\t\treturn () => set!.delete(handler);\n\t}\n}\n\n// ── Rate limit store pluggable ──\nexport interface WSRateLimitStore {\n\tincr(key: string, windowMs: number): Promise<number> | number;\n}\n\nexport class MemoryWSRateLimitStore implements WSRateLimitStore {\n\tprivate map = new Map<string, { count: number; resetAt: number }>();\n\tincr(key: string, windowMs: number): number {\n\t\tconst now = Date.now();\n\t\tconst ent = this.map.get(key);\n\t\tif (!ent || now > ent.resetAt) {\n\t\t\tthis.map.set(key, { count: 1, resetAt: now + windowMs });\n\t\t\treturn 1;\n\t\t}\n\t\tent.count++;\n\t\treturn ent.count;\n\t}\n}\n\n/**\n * Validate a WebSocket message against a Zod schema.\n */\nexport function validateWSMessage<T>(\n\tschema: z.ZodSchema<T>,\n\tmessage: string | Buffer,\n): { success: true; data: T } | { success: false; errors: z.ZodError } {\n\tconst raw = typeof message === \"string\" ? message : message.toString();\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terrors: new z.ZodError([\n\t\t\t\t{\n\t\t\t\t\tcode: \"custom\",\n\t\t\t\t\tmessage: \"Invalid JSON\",\n\t\t\t\t\tpath: [],\n\t\t\t\t},\n\t\t\t]),\n\t\t};\n\t}\n\n\tconst result = schema.safeParse(parsed);\n\tif (result.success) {\n\t\treturn { success: true, data: result.data };\n\t}\n\treturn { success: false, errors: result.error };\n}\n\n/**\n * Room class - in-memory default, pluggable ke Redis via pubSub\n * Mirip LocalDiskStorage vs MemoryStorage pattern.\n *\n * @example Memory (default)\n * ```ts\n * const room = new Room(\"chat\");\n * app.ws(\"/chat\", { open: (ws)=> room.join(ws), message: (ws,msg)=> room.broadcast(msg, ws) })\n * ```\n * @example Redis (pluggable)\n * ```ts\n * class RedisWSPubSub implements WSPubSub { constructor(private redis: Redis){} publish(ch,msg){ redis.publish(ch, typeof msg===\"object\"?JSON.stringify(msg):msg as string)} subscribe(ch,cb){ redis.subscribe(ch,(m)=>cb(m)); return ()=>redis.unsubscribe(ch)} }\n * const room = new Room(\"chat\", { pubSub: new RedisWSPubSub(redis) })\n * ```\n */\nexport interface RoomOptions {\n\tpubSub?: WSPubSub;\n\tchannel?: string;\n}\nexport class Room<DI = Record<string, unknown>> {\n\tpublic readonly name: string;\n\tprivate members = new Set<ServerWebSocket<WSData<DI>>>();\n\tprivate pubSub?: WSPubSub;\n\tprivate channel: string;\n\tprivate unsubscribe?: () => void;\n\n\tconstructor(name: string, opts?: RoomOptions | WSPubSub) {\n\t\tthis.name = name;\n\t\tif (opts && typeof (opts as WSPubSub).publish === \"function\") {\n\t\t\tthis.pubSub = opts as WSPubSub;\n\t\t\tthis.channel = `buntok:ws:${name}`;\n\t\t} else {\n\t\t\tconst o = opts as RoomOptions | undefined;\n\t\t\tthis.pubSub = o?.pubSub;\n\t\t\tthis.channel = o?.channel ?? `buntok:ws:${name}`;\n\t\t}\n\t\tif (this.pubSub) {\n\t\t\tthis.unsubscribe = this.pubSub.subscribe(this.channel, (msg) => {\n\t\t\t\tconst data = typeof msg === \"object\" ? JSON.stringify(msg) : (msg as string);\n\t\t\t\tfor (const m of [...this.members]) {\n\t\t\t\t\tif (m.readyState === 1) {\n\t\t\t\t\t\ttry { m.send(data); } catch {}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\tjoin(ws: ServerWebSocket<WSData<DI>>): void {\n\t\tthis.members.add(ws);\n\t\t// Also Bun native subscribe untuk fan-out kernel (optional, not required jika pakai pubSub)\n\t\ttry { (ws as unknown as { subscribe?: (c:string)=>void }).subscribe?.(this.channel); } catch {}\n\t}\n\n\tleave(ws: ServerWebSocket<WSData<DI>>): void {\n\t\tthis.members.delete(ws);\n\t\ttry { (ws as unknown as { unsubscribe?: (c:string)=>void }).unsubscribe?.(this.channel); } catch {}\n\t}\n\n\tbroadcast(message: string | object, exclude?: ServerWebSocket<WSData<DI>>): void {\n\t\tif (this.pubSub) {\n\t\t\t// via pubSub (Redis) - akan di-fanout ke semua instance termasuk self via subscribe\n\t\t\tconst r = this.pubSub.publish(this.channel, message);\n\t\t\tif (r instanceof Promise) r.catch(() => this.localBroadcast(message, exclude));\n\t\t\treturn;\n\t\t}\n\t\tthis.localBroadcast(message, exclude);\n\t}\n\n\tprivate localBroadcast(message: string | object, exclude?: ServerWebSocket<WSData<DI>>): void {\n\t\tconst data = typeof message === \"object\" ? JSON.stringify(message) : message;\n\t\t// Backpressure check: jika bufferedAmount tinggi, skip slow client\n\t\tfor (const member of [...this.members]) {\n\t\t\tif (member !== exclude && member.readyState === 1) {\n\t\t\t\ttry {\n\t\t\t\t\tconst buffered = (member as unknown as { getBufferedAmount?: () => number }).getBufferedAmount?.() ?? 0;\n\t\t\t\t\tif (buffered > 1024 * 1024) continue; // drop jika >1MB buffered\n\t\t\t\t\tmember.send(data);\n\t\t\t\t} catch {}\n\t\t\t}\n\t\t}\n\t}\n\n\tsendAll(message: string | object): void {\n\t\tthis.broadcast(message);\n\t}\n\n\tgetMembers(): ServerWebSocket<WSData<DI>>[] {\n\t\treturn Array.from(this.members);\n\t}\n\n\tget size(): number {\n\t\treturn this.members.size;\n\t}\n\n\tget isEmpty(): boolean {\n\t\treturn this.members.size === 0;\n\t}\n\n\thas(ws: ServerWebSocket<WSData<DI>>): boolean {\n\t\treturn this.members.has(ws);\n\t}\n\n\tcloseAll(): void {\n\t\tif (this.unsubscribe) { try { this.unsubscribe(); } catch {} }\n\t\tfor (const member of [...this.members]) {\n\t\t\ttry { member.close(1000, \"Room closed\"); } catch {}\n\t\t}\n\t\tthis.members.clear();\n\t}\n\n\t/** Filtered broadcast mirip SSEBroadcaster.broadcastWhere */\n\tbroadcastWhere(predicate: (ws: ServerWebSocket<WSData<DI>>) => boolean, message: string | object): void {\n\t\tconst data = typeof message === \"object\" ? JSON.stringify(message) : message;\n\t\tfor (const member of [...this.members]) {\n\t\t\tif (member.readyState !== 1) continue;\n\t\t\tif (predicate(member)) {\n\t\t\t\ttry { member.send(data); } catch {}\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * WebSocket heartbeat - 30s fixed, Bun-only\n * Fix: pong event reset alive (bukan message/drain)\n */\nexport function wsHeartbeat<DI = Record<string, unknown>>(\n\tinterval = 30_000,\n): WSHandler<DI> {\n\treturn {\n\t\topen: (ws: ServerWebSocket<WSData<DI>>) => {\n\t\t\tconst wsData = ws.data as WSDataWithHeartbeat<DI>;\n\t\t\twsData.heartbeat = {\n\t\t\t\tinterval,\n\t\t\t\ttimer: null as ReturnType<typeof setInterval> | null,\n\t\t\t\talive: true,\n\t\t\t};\n\t\t\twsData.heartbeat.timer = setInterval(() => {\n\t\t\t\tconst hb = (ws.data as WSDataWithHeartbeat<DI>).heartbeat;\n\t\t\t\tif (!hb) return;\n\t\t\t\tif (!hb.alive) {\n\t\t\t\t\ttry { ws.close(4000, \"Heartbeat timeout\"); } catch {}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\thb.alive = false;\n\t\t\t\ttry { ws.ping(); } catch {}\n\t\t\t}, interval);\n\t\t},\n\t\t// `pong` handler khusus - akan di-wire di app.ts websocket.pong\n\t\t// fallback untuk Bun versi lama: message juga reset (compat)\n\t\tmessage: (ws: ServerWebSocket<WSData<DI>>) => {\n\t\t\tconst wsData = ws.data as WSDataWithHeartbeat<DI>;\n\t\t\tif (wsData.heartbeat) wsData.heartbeat.alive = true;\n\t\t},\n\t\tclose: (ws: ServerWebSocket<WSData<DI>>) => {\n\t\t\tconst wsData = ws.data as WSDataWithHeartbeat<DI>;\n\t\t\tif (wsData.heartbeat?.timer) clearInterval(wsData.heartbeat.timer);\n\t\t},\n\t\t// drain tidak reset alive lagi (fix drain bug)\n\t} as unknown as WSHandler<DI>;\n}\n\n/** Internal helper untuk app.ts - reset pong */\nexport function wsHeartbeatPong<DI>(ws: ServerWebSocket<WSData<DI>>): void {\n\tconst wsData = ws.data as unknown as WSDataWithHeartbeat<DI>;\n\tif (wsData.heartbeat) wsData.heartbeat.alive = true;\n}\n\n/**\n * WebSocket rate limiter - in-memory default, pluggable ke Redis\n * @example\n * const limiter = wsRateLimit({ windowMs: 1000, max: 10 })\n * app.ws(\"/chat\", { ...limiter, message: (ws,msg)=>{} })\n */\nexport interface WSRateLimitOptions<DI = Record<string, unknown>> {\n\twindowMs?: number;\n\tmax?: number;\n\tstore?: WSRateLimitStore;\n\tkeyGenerator?: (ws: ServerWebSocket<WSData<DI>>) => string;\n\t/** Close code saat limit */\n\tcloseCode?: number;\n}\nexport function wsRateLimit<DI = Record<string, unknown>>(opts: WSRateLimitOptions<DI> = {}): WSHandler<DI> {\n\tconst windowMs = opts.windowMs ?? 1000;\n\tconst max = opts.max ?? 20;\n\tconst store = opts.store ?? new MemoryWSRateLimitStore();\n\tconst keyGen = opts.keyGenerator ?? ((ws: ServerWebSocket<WSData<DI>>) => (ws.data.ctx.ip || \"unknown\"));\n\tconst closeCode = opts.closeCode ?? 1013;\n\treturn {\n\t\tmessage: (ws: ServerWebSocket<WSData<DI>>) => {\n\t\t\tconst key = keyGen(ws);\n\t\t\tconst count = store.incr(key, windowMs);\n\t\t\tconst c = count instanceof Promise ? 0 : count;\n\t\t\t// async case handled via promise\n\t\t\tif (count instanceof Promise) {\n\t\t\t\tcount.then((n) => { if (n > max) try { ws.close(closeCode, \"Rate limited\"); } catch {} });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (c > max) {\n\t\t\t\ttry { ws.close(closeCode, \"Rate limited\"); } catch {}\n\t\t\t}\n\t\t},\n\t} as WSHandler<DI>;\n}\n\n/**\n * WebSocket authentication middleware - pluggable\n */\nexport function wsAuth<DI extends Record<string, unknown>, TAuth = unknown>(\n\tauthenticate: (ws: ServerWebSocket<WSData<DI>>) => Promise<TAuth | null>,\n): WSHandler<DI> {\n\treturn {\n\t\topen: async (ws) => {\n\t\t\ttry {\n\t\t\t\tconst authData = await authenticate(ws);\n\t\t\t\tif (authData === null) {\n\t\t\t\t\tws.close(4001, \"Unauthorized\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t(ws.data as unknown as Record<string, unknown>).auth = authData;\n\t\t\t} catch {\n\t\t\t\tws.close(4001, \"Unauthorized\");\n\t\t\t}\n\t\t},\n\t} as WSHandler<DI>;\n}\n"]}