@stacksjs/realtime 0.72.38 → 0.72.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/broadcast.js CHANGED
@@ -1 +1 @@
1
- import{log}from"@stacksjs/logging";import{recordBroadcast}from"./replay-buffer";import{getServer}from"./server-instance";let backpressureConfig=null;export function setBackpressureGuard(cfg){if(!cfg){backpressureConfig=null;return}backpressureConfig={maxPerSocketBytes:cfg.maxPerSocketBytes??1048576,onSlow:cfg.onSlow??((info)=>{log.warn(`[realtime] slow consumer on '${info.channelName}': ${info.backpressure} bytes buffered`)})}}export function getBackpressureGuard(){return backpressureConfig}function checkBackpressure(server,channelName){if(!backpressureConfig)return;try{const channels=server.channels??server.clients,set=channels&&typeof channels.get==="function"?channels.get(channelName):null;if(!set||typeof set[Symbol.iterator]!=="function")return;const{maxPerSocketBytes,onSlow}=backpressureConfig;for(const entry of set){const ws=entry&&typeof entry==="object"&&"ws"in entry?entry.ws:entry,bp=ws&&typeof ws==="object"&&"backpressure"in ws?ws.backpressure:null;if(typeof bp==="number"&&bp>maxPerSocketBytes)onSlow({channelName,backpressure:bp,socket:ws})}}catch{}}function hasSubscribers(server,channelName){try{if(typeof server.hasSubscribers==="function")return Boolean(server.hasSubscribers(channelName));if(typeof server.subscriberCount==="function")return server.subscriberCount(channelName)>0;const channels=server.channels??server.clients;if(channels&&typeof channels.get==="function"){const set=channels.get(channelName),size=(set&&(set.size??set.length))??null;if(typeof size==="number")return size>0}}catch{}return!0}export class Broadcast{async connect(){}async disconnect(){}subscribe(channel,callback){log.warn("Broadcast.subscribe() is a client-side operation. Use BroadcastClient instead.")}unsubscribe(channel){log.warn("Broadcast.unsubscribe() is a client-side operation. Use BroadcastClient instead.")}broadcast(channel,event,data,type="public"){const server=getServer();if(!server){log.warn("Broadcast server not initialized");return}let channelName=channel;if(type==="private"&&!channel.startsWith("private-"))channelName=`private-${channel}`;else if(type==="presence"&&!channel.startsWith("presence-"))channelName=`presence-${channel}`;if(!hasSubscribers(server,channelName)){log.debug(`[Broadcast] Skipping '${event}' on '${channelName}' \u2014 no subscribers`);return}checkBackpressure(server,channelName);recordBroadcast(channelName,event,data);try{server.broadcast(channelName,event,data)}catch(err){log.error(`[Broadcast] Failed to broadcast event '${event}' to channel '${channelName}':`,err)}}isConnected(){return getServer()!==null}}export async function runBroadcast(name,payload){const{appPath}=await import("@stacksjs/path"),bun=await import("bun");let broadcastFiles;try{broadcastFiles=bun.globSync([appPath("Broadcasts/**/*.ts")],{absolute:!0})}catch(error){throw Error(`Failed to scan broadcast files: ${error instanceof Error?error.message:String(error)}`)}const broadcastFile=broadcastFiles.find((file)=>file.endsWith(`${name}.ts`));if(!broadcastFile)throw Error(`Broadcast ${name} not found`);let broadcastModule;try{broadcastModule=await import(broadcastFile)}catch(error){throw Error(`Failed to import broadcast '${name}': ${error instanceof Error?error.message:String(error)}`)}const instance=broadcastModule.default;if(instance.handle){await instance.handle(payload);return}const server=getServer();if(!server)throw Error("Broadcast server not initialized");const channels=instance.broadcastOn?.()||instance.channel?.()||[],eventName=instance.broadcastAs?.()||instance.event?.()||name,data=instance.broadcastWith?.()||instance.data?.()||payload,event={shouldBroadcast:()=>!0,broadcastOn:()=>channels,broadcastAs:()=>eventName,broadcastWith:()=>data};await server.broadcaster.broadcast(event)}export async function broadcast(name,payload){if(typeof name!=="string"||name.trim().length===0)throw Error("[realtime] broadcast() requires a non-empty event name");await runBroadcast(name,payload)}
1
+ import{log}from"@stacksjs/logging";import{recordBroadcast}from"./replay-buffer";import{getServer}from"./server-instance";let backpressureConfig=null;export function setBackpressureGuard(cfg){if(!cfg){backpressureConfig=null;return}backpressureConfig={maxPerSocketBytes:cfg.maxPerSocketBytes??1048576,onSlow:cfg.onSlow??((info)=>{log.warn(`[realtime] slow consumer on '${info.channelName}': ${info.backpressure} bytes buffered`)})}}export function getBackpressureGuard(){return backpressureConfig}function checkBackpressure(server,channelName){if(!backpressureConfig)return;try{const channels=server.channels??server.clients,set=channels&&typeof channels.get==="function"?channels.get(channelName):null;if(!set||typeof set[Symbol.iterator]!=="function")return;const{maxPerSocketBytes,onSlow}=backpressureConfig;for(const entry of set){const ws=entry&&typeof entry==="object"&&"ws"in entry?entry.ws:entry,bp=ws&&typeof ws==="object"&&"backpressure"in ws?ws.backpressure:null;if(typeof bp==="number"&&bp>maxPerSocketBytes)onSlow({channelName,backpressure:bp,socket:ws})}}catch{}}function hasSubscribers(server,channelName){try{if(typeof server.hasSubscribers==="function")return Boolean(server.hasSubscribers(channelName));if(typeof server.subscriberCount==="function")return server.subscriberCount(channelName)>0;const channels=server.channels??server.clients;if(channels&&typeof channels.get==="function"){const set=channels.get(channelName),size=(set&&(set.size??set.length))??null;if(typeof size==="number")return size>0}}catch{}return!0}export class Broadcast{async connect(){}async disconnect(){}subscribe(channel,callback){log.warn("Broadcast.subscribe() is a client-side operation. Use BroadcastClient instead.")}unsubscribe(channel){log.warn("Broadcast.unsubscribe() is a client-side operation. Use BroadcastClient instead.")}broadcast(channel,event,data,type="public"){const server=getServer();if(!server){log.warn("Broadcast server not initialized");return}let channelName=channel;if(type==="private"&&!channel.startsWith("private-"))channelName=`private-${channel}`;else if(type==="presence"&&!channel.startsWith("presence-"))channelName=`presence-${channel}`;if(!hasSubscribers(server,channelName)){log.debug(`[Broadcast] Skipping '${event}' on '${channelName}' - no subscribers`);return}checkBackpressure(server,channelName);recordBroadcast(channelName,event,data);try{server.broadcast(channelName,event,data)}catch(err){log.error(`[Broadcast] Failed to broadcast event '${event}' to channel '${channelName}':`,err)}}isConnected(){return getServer()!==null}}export async function runBroadcast(name,payload){const{appPath}=await import("@stacksjs/path"),bun=await import("bun");let broadcastFiles;try{broadcastFiles=bun.globSync([appPath("Broadcasts/**/*.ts")],{absolute:!0})}catch(error){throw Error(`Failed to scan broadcast files: ${error instanceof Error?error.message:String(error)}`)}const broadcastFile=broadcastFiles.find((file)=>file.endsWith(`${name}.ts`));if(!broadcastFile)throw Error(`Broadcast ${name} not found`);let broadcastModule;try{broadcastModule=await import(broadcastFile)}catch(error){throw Error(`Failed to import broadcast '${name}': ${error instanceof Error?error.message:String(error)}`)}const instance=broadcastModule.default;if(instance.handle){await instance.handle(payload);return}const server=getServer();if(!server)throw Error("Broadcast server not initialized");const channels=instance.broadcastOn?.()||instance.channel?.()||[],eventName=instance.broadcastAs?.()||instance.event?.()||name,data=instance.broadcastWith?.()||instance.data?.()||payload,event={shouldBroadcast:()=>!0,broadcastOn:()=>channels,broadcastAs:()=>eventName,broadcastWith:()=>data};await server.broadcaster.broadcast(event)}export async function broadcast(name,payload){if(typeof name!=="string"||name.trim().length===0)throw Error("[realtime] broadcast() requires a non-empty event name");await runBroadcast(name,payload)}
package/dist/heartbeat.js CHANGED
@@ -1 +1 @@
1
- import{log}from"@stacksjs/logging";import{getServer}from"./server-instance";let state=null;export function setHeartbeatConfig(cfg){if(state?.timer){clearInterval(state.timer);state.timer=null}if(!cfg){state=null;return}const intervalMs=cfg.intervalMs??30000,maxMissedPongs=cfg.maxMissedPongs??2,onDead=cfg.onDead??defaultOnDead;state={intervalMs,maxMissedPongs,onDead,missed:new WeakMap,timer:null};state.timer=setInterval(()=>{if(!state)return;runOneTick()},intervalMs);state.timer.unref?.()}export function getHeartbeatConfig(){return state}export function runOneTick(){if(!state)return;const server=getServer();if(!server)return;const allSockets=collectSockets(server);for(const socket of allSockets){const ws=socket,missed=state.missed.get(socket)??0;if(missed>=state.maxMissedPongs){log.warn(`[realtime] socket missed ${missed} pongs \u2014 declaring dead`);try{state.onDead(socket)}catch(err){log.warn(`[realtime] heartbeat onDead handler threw: ${err instanceof Error?err.message:String(err)}`)}state.missed.delete(socket);continue}state.missed.set(socket,missed+1);try{if(typeof ws.ping==="function")ws.ping();else if(typeof ws.send==="function")ws.send("__stacks_ping__")}catch{}}}export function markPong(socket){if(!state)return;state.missed.delete(socket)}function collectSockets(server){const out=new Set;try{const channels=server.channels??server.clients;if(channels&&typeof channels.values==="function"){for(const set of channels.values())if(set&&typeof set[Symbol.iterator]==="function")for(const entry of set){const ws=entry&&typeof entry==="object"&&"ws"in entry?entry.ws:entry;if(ws&&typeof ws==="object")out.add(ws)}}}catch{}return[...out]}function defaultOnDead(socket){const ws=socket;if(typeof ws.close==="function")ws.close(1011,"heartbeat timeout")}
1
+ import{log}from"@stacksjs/logging";import{getServer}from"./server-instance";let state=null;export function setHeartbeatConfig(cfg){if(state?.timer){clearInterval(state.timer);state.timer=null}if(!cfg){state=null;return}const intervalMs=cfg.intervalMs??30000,maxMissedPongs=cfg.maxMissedPongs??2,onDead=cfg.onDead??defaultOnDead;state={intervalMs,maxMissedPongs,onDead,missed:new WeakMap,timer:null};state.timer=setInterval(()=>{if(!state)return;runOneTick()},intervalMs);state.timer.unref?.()}export function getHeartbeatConfig(){return state}export function runOneTick(){if(!state)return;const server=getServer();if(!server)return;const allSockets=collectSockets(server);for(const socket of allSockets){const ws=socket,missed=state.missed.get(socket)??0;if(missed>=state.maxMissedPongs){log.warn(`[realtime] socket missed ${missed} pongs - declaring dead`);try{state.onDead(socket)}catch(err){log.warn(`[realtime] heartbeat onDead handler threw: ${err instanceof Error?err.message:String(err)}`)}state.missed.delete(socket);continue}state.missed.set(socket,missed+1);try{if(typeof ws.ping==="function")ws.ping();else if(typeof ws.send==="function")ws.send("__stacks_ping__")}catch{}}}export function markPong(socket){if(!state)return;state.missed.delete(socket)}function collectSockets(server){const out=new Set;try{const channels=server.channels??server.clients;if(channels&&typeof channels.values==="function"){for(const set of channels.values())if(set&&typeof set[Symbol.iterator]==="function")for(const entry of set){const ws=entry&&typeof entry==="object"&&"ws"in entry?entry.ws:entry;if(ws&&typeof ws==="object")out.add(ws)}}}catch{}return[...out]}function defaultOnDead(socket){const ws=socket;if(typeof ws.close==="function")ws.close(1011,"heartbeat timeout")}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/realtime",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.72.38",
5
+ "version": "0.72.40",
6
6
  "description": "The Stacks realtime integration. Built on top of ts-broadcasting.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [