@stacksjs/realtime 0.70.258 → 0.70.259
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 +1 -124
- package/dist/channel.js +1 -46
- package/dist/emit.js +1 -25
- package/dist/heartbeat.js +1 -84
- package/dist/index.js +1 -10
- package/dist/replay-buffer.js +1 -81
- package/dist/server-instance.js +1 -19
- package/dist/ws.js +1 -28
- package/package.json +11 -4
package/dist/broadcast.js
CHANGED
|
@@ -1,124 +1 @@
|
|
|
1
|
-
import { log }
|
|
2
|
-
import { recordBroadcast } from "./replay-buffer";
|
|
3
|
-
import { getServer } from "./server-instance";
|
|
4
|
-
let backpressureConfig = null;
|
|
5
|
-
export function setBackpressureGuard(cfg) {
|
|
6
|
-
if (!cfg) {
|
|
7
|
-
backpressureConfig = null;
|
|
8
|
-
return;
|
|
9
|
-
}
|
|
10
|
-
backpressureConfig = {
|
|
11
|
-
maxPerSocketBytes: cfg.maxPerSocketBytes ?? 1048576,
|
|
12
|
-
onSlow: cfg.onSlow ?? ((info) => {
|
|
13
|
-
log.warn(`[realtime] slow consumer on '${info.channelName}': ${info.backpressure} bytes buffered`);
|
|
14
|
-
})
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
export function getBackpressureGuard() {
|
|
18
|
-
return backpressureConfig;
|
|
19
|
-
}
|
|
20
|
-
function checkBackpressure(server, channelName) {
|
|
21
|
-
if (!backpressureConfig)
|
|
22
|
-
return;
|
|
23
|
-
try {
|
|
24
|
-
const channels = server.channels ?? server.clients, set = channels && typeof channels.get === "function" ? channels.get(channelName) : null;
|
|
25
|
-
if (!set || typeof set[Symbol.iterator] !== "function")
|
|
26
|
-
return;
|
|
27
|
-
const { maxPerSocketBytes, onSlow } = backpressureConfig;
|
|
28
|
-
for (const entry of set) {
|
|
29
|
-
const ws = entry && typeof entry === "object" && "ws" in entry ? entry.ws : entry, bp = ws && typeof ws === "object" && "backpressure" in ws ? ws.backpressure : null;
|
|
30
|
-
if (typeof bp === "number" && bp > maxPerSocketBytes)
|
|
31
|
-
onSlow({ channelName, backpressure: bp, socket: ws });
|
|
32
|
-
}
|
|
33
|
-
} catch {}
|
|
34
|
-
}
|
|
35
|
-
function hasSubscribers(server, channelName) {
|
|
36
|
-
try {
|
|
37
|
-
if (typeof server.hasSubscribers === "function")
|
|
38
|
-
return Boolean(server.hasSubscribers(channelName));
|
|
39
|
-
if (typeof server.subscriberCount === "function")
|
|
40
|
-
return server.subscriberCount(channelName) > 0;
|
|
41
|
-
const channels = server.channels ?? server.clients;
|
|
42
|
-
if (channels && typeof channels.get === "function") {
|
|
43
|
-
const set = channels.get(channelName), size = (set && (set.size ?? set.length)) ?? null;
|
|
44
|
-
if (typeof size === "number")
|
|
45
|
-
return size > 0;
|
|
46
|
-
}
|
|
47
|
-
} catch {}
|
|
48
|
-
return !0;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export class Broadcast {
|
|
52
|
-
async connect() {}
|
|
53
|
-
async disconnect() {}
|
|
54
|
-
subscribe(channel, callback) {
|
|
55
|
-
log.warn("Broadcast.subscribe() is a client-side operation. Use BroadcastClient instead.");
|
|
56
|
-
}
|
|
57
|
-
unsubscribe(channel) {
|
|
58
|
-
log.warn("Broadcast.unsubscribe() is a client-side operation. Use BroadcastClient instead.");
|
|
59
|
-
}
|
|
60
|
-
broadcast(channel, event, data, type = "public") {
|
|
61
|
-
const server = getServer();
|
|
62
|
-
if (!server) {
|
|
63
|
-
log.warn("Broadcast server not initialized");
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
let channelName = channel;
|
|
67
|
-
if (type === "private" && !channel.startsWith("private-"))
|
|
68
|
-
channelName = `private-${channel}`;
|
|
69
|
-
else if (type === "presence" && !channel.startsWith("presence-"))
|
|
70
|
-
channelName = `presence-${channel}`;
|
|
71
|
-
if (!hasSubscribers(server, channelName)) {
|
|
72
|
-
log.debug(`[Broadcast] Skipping '${event}' on '${channelName}' \u2014 no subscribers`);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
checkBackpressure(server, channelName);
|
|
76
|
-
recordBroadcast(channelName, event, data);
|
|
77
|
-
try {
|
|
78
|
-
server.broadcast(channelName, event, data);
|
|
79
|
-
} catch (err) {
|
|
80
|
-
log.error(`[Broadcast] Failed to broadcast event '${event}' to channel '${channelName}':`, err);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
isConnected() {
|
|
84
|
-
return getServer() !== null;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
export async function runBroadcast(name, payload) {
|
|
88
|
-
const { appPath } = await import("@stacksjs/path"), bun = await import("bun");
|
|
89
|
-
let broadcastFiles;
|
|
90
|
-
try {
|
|
91
|
-
broadcastFiles = bun.globSync([appPath("Broadcasts/**/*.ts")], { absolute: !0 });
|
|
92
|
-
} catch (error) {
|
|
93
|
-
throw Error(`Failed to scan broadcast files: ${error instanceof Error ? error.message : String(error)}`);
|
|
94
|
-
}
|
|
95
|
-
const broadcastFile = broadcastFiles.find((file) => file.endsWith(`${name}.ts`));
|
|
96
|
-
if (!broadcastFile)
|
|
97
|
-
throw Error(`Broadcast ${name} not found`);
|
|
98
|
-
let broadcastModule;
|
|
99
|
-
try {
|
|
100
|
-
broadcastModule = await import(broadcastFile);
|
|
101
|
-
} catch (error) {
|
|
102
|
-
throw Error(`Failed to import broadcast '${name}': ${error instanceof Error ? error.message : String(error)}`);
|
|
103
|
-
}
|
|
104
|
-
const instance = broadcastModule.default;
|
|
105
|
-
if (instance.handle) {
|
|
106
|
-
await instance.handle(payload);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
const server = getServer();
|
|
110
|
-
if (!server)
|
|
111
|
-
throw Error("Broadcast server not initialized");
|
|
112
|
-
const channels = instance.broadcastOn?.() || instance.channel?.() || [], eventName = instance.broadcastAs?.() || instance.event?.() || name, data = instance.broadcastWith?.() || instance.data?.() || payload, event = {
|
|
113
|
-
shouldBroadcast: () => !0,
|
|
114
|
-
broadcastOn: () => channels,
|
|
115
|
-
broadcastAs: () => eventName,
|
|
116
|
-
broadcastWith: () => data
|
|
117
|
-
};
|
|
118
|
-
await server.broadcaster.broadcast(event);
|
|
119
|
-
}
|
|
120
|
-
export async function broadcast(name, payload) {
|
|
121
|
-
if (typeof name !== "string" || name.trim().length === 0)
|
|
122
|
-
throw Error("[realtime] broadcast() requires a non-empty event name");
|
|
123
|
-
await runBroadcast(name, payload);
|
|
124
|
-
}
|
|
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)}
|
package/dist/channel.js
CHANGED
|
@@ -1,46 +1 @@
|
|
|
1
|
-
import { getServer }
|
|
2
|
-
const KNOWN_CHANNEL_PREFIXES = ["private-", "presence-"];
|
|
3
|
-
function stripPrefix(name) {
|
|
4
|
-
for (const p of KNOWN_CHANNEL_PREFIXES)
|
|
5
|
-
if (name.startsWith(p))
|
|
6
|
-
return name.slice(p.length);
|
|
7
|
-
return name;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export class Channel {
|
|
11
|
-
channelName;
|
|
12
|
-
constructor(channel) {
|
|
13
|
-
this.channelName = channel;
|
|
14
|
-
}
|
|
15
|
-
async private(event, data) {
|
|
16
|
-
const server = getServer();
|
|
17
|
-
if (!server)
|
|
18
|
-
throw Error("Broadcast server not initialized");
|
|
19
|
-
await server.broadcast(`private-${stripPrefix(this.channelName)}`, event, data);
|
|
20
|
-
}
|
|
21
|
-
async public(event, data) {
|
|
22
|
-
const server = getServer();
|
|
23
|
-
if (!server)
|
|
24
|
-
throw Error("Broadcast server not initialized");
|
|
25
|
-
await server.broadcast(stripPrefix(this.channelName), event, data);
|
|
26
|
-
}
|
|
27
|
-
async presence(event, data) {
|
|
28
|
-
const server = getServer();
|
|
29
|
-
if (!server)
|
|
30
|
-
throw Error("Broadcast server not initialized");
|
|
31
|
-
await server.broadcast(`presence-${stripPrefix(this.channelName)}`, event, data);
|
|
32
|
-
}
|
|
33
|
-
async broadcast(event, data, type = "public") {
|
|
34
|
-
switch (type) {
|
|
35
|
-
case "private":
|
|
36
|
-
return this.private(event, data);
|
|
37
|
-
case "presence":
|
|
38
|
-
return this.presence(event, data);
|
|
39
|
-
default:
|
|
40
|
-
return this.public(event, data);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
export function channel(name) {
|
|
45
|
-
return new Channel(name);
|
|
46
|
-
}
|
|
1
|
+
import{getServer}from"./server-instance";const KNOWN_CHANNEL_PREFIXES=["private-","presence-"];function stripPrefix(name){for(const p of KNOWN_CHANNEL_PREFIXES)if(name.startsWith(p))return name.slice(p.length);return name}export class Channel{channelName;constructor(channel){this.channelName=channel}async private(event,data){const server=getServer();if(!server)throw Error("Broadcast server not initialized");await server.broadcast(`private-${stripPrefix(this.channelName)}`,event,data)}async public(event,data){const server=getServer();if(!server)throw Error("Broadcast server not initialized");await server.broadcast(stripPrefix(this.channelName),event,data)}async presence(event,data){const server=getServer();if(!server)throw Error("Broadcast server not initialized");await server.broadcast(`presence-${stripPrefix(this.channelName)}`,event,data)}async broadcast(event,data,type="public"){switch(type){case"private":return this.private(event,data);case"presence":return this.presence(event,data);default:return this.public(event,data)}}}export function channel(name){return new Channel(name)}
|
package/dist/emit.js
CHANGED
|
@@ -1,25 +1 @@
|
|
|
1
|
-
import { getServer }
|
|
2
|
-
export function emit(channel, event, data, options) {
|
|
3
|
-
const server = getServer();
|
|
4
|
-
if (!server) {
|
|
5
|
-
console.warn("[realtime] Server not initialized, cannot emit event");
|
|
6
|
-
return;
|
|
7
|
-
}
|
|
8
|
-
let channelName = channel;
|
|
9
|
-
if (options?.presence) {
|
|
10
|
-
if (!channel.startsWith("presence-"))
|
|
11
|
-
channelName = `presence-${channel}`;
|
|
12
|
-
} else if (options?.private) {
|
|
13
|
-
if (!channel.startsWith("private-"))
|
|
14
|
-
channelName = `private-${channel}`;
|
|
15
|
-
}
|
|
16
|
-
const excludeSocketId = options?.exclude ? Array.isArray(options.exclude) ? options.exclude[0] : options.exclude : void 0;
|
|
17
|
-
server.broadcast(channelName, event, data, excludeSocketId);
|
|
18
|
-
}
|
|
19
|
-
export function emitToUser(userId, event, data, options) {
|
|
20
|
-
emit(`private-user.${userId}`, event, data, { ...options, private: !0 });
|
|
21
|
-
}
|
|
22
|
-
export function emitToUsers(userIds, event, data, options) {
|
|
23
|
-
for (const userId of userIds)
|
|
24
|
-
emitToUser(userId, event, data, options);
|
|
25
|
-
}
|
|
1
|
+
import{getServer}from"./server-instance";export function emit(channel,event,data,options){const server=getServer();if(!server){console.warn("[realtime] Server not initialized, cannot emit event");return}let channelName=channel;if(options?.presence){if(!channel.startsWith("presence-"))channelName=`presence-${channel}`}else if(options?.private){if(!channel.startsWith("private-"))channelName=`private-${channel}`}const excludeSocketId=options?.exclude?Array.isArray(options.exclude)?options.exclude[0]:options.exclude:void 0;server.broadcast(channelName,event,data,excludeSocketId)}export function emitToUser(userId,event,data,options){emit(`private-user.${userId}`,event,data,{...options,private:!0})}export function emitToUsers(userIds,event,data,options){for(const userId of userIds)emitToUser(userId,event,data,options)}
|
package/dist/heartbeat.js
CHANGED
|
@@ -1,84 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { getServer } from "./server-instance";
|
|
3
|
-
let state = null;
|
|
4
|
-
export function setHeartbeatConfig(cfg) {
|
|
5
|
-
if (state?.timer) {
|
|
6
|
-
clearInterval(state.timer);
|
|
7
|
-
state.timer = null;
|
|
8
|
-
}
|
|
9
|
-
if (!cfg) {
|
|
10
|
-
state = null;
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
const intervalMs = cfg.intervalMs ?? 30000, maxMissedPongs = cfg.maxMissedPongs ?? 2, onDead = cfg.onDead ?? defaultOnDead;
|
|
14
|
-
state = {
|
|
15
|
-
intervalMs,
|
|
16
|
-
maxMissedPongs,
|
|
17
|
-
onDead,
|
|
18
|
-
missed: new WeakMap,
|
|
19
|
-
timer: null
|
|
20
|
-
};
|
|
21
|
-
state.timer = setInterval(() => {
|
|
22
|
-
if (!state)
|
|
23
|
-
return;
|
|
24
|
-
runOneTick();
|
|
25
|
-
}, intervalMs);
|
|
26
|
-
state.timer.unref?.();
|
|
27
|
-
}
|
|
28
|
-
export function getHeartbeatConfig() {
|
|
29
|
-
return state;
|
|
30
|
-
}
|
|
31
|
-
export function runOneTick() {
|
|
32
|
-
if (!state)
|
|
33
|
-
return;
|
|
34
|
-
const server = getServer();
|
|
35
|
-
if (!server)
|
|
36
|
-
return;
|
|
37
|
-
const allSockets = collectSockets(server);
|
|
38
|
-
for (const socket of allSockets) {
|
|
39
|
-
const ws = socket, missed = state.missed.get(socket) ?? 0;
|
|
40
|
-
if (missed >= state.maxMissedPongs) {
|
|
41
|
-
log.warn(`[realtime] socket missed ${missed} pongs \u2014 declaring dead`);
|
|
42
|
-
try {
|
|
43
|
-
state.onDead(socket);
|
|
44
|
-
} catch (err) {
|
|
45
|
-
log.warn(`[realtime] heartbeat onDead handler threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
46
|
-
}
|
|
47
|
-
state.missed.delete(socket);
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
state.missed.set(socket, missed + 1);
|
|
51
|
-
try {
|
|
52
|
-
if (typeof ws.ping === "function")
|
|
53
|
-
ws.ping();
|
|
54
|
-
else if (typeof ws.send === "function")
|
|
55
|
-
ws.send("__stacks_ping__");
|
|
56
|
-
} catch {}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
export function markPong(socket) {
|
|
60
|
-
if (!state)
|
|
61
|
-
return;
|
|
62
|
-
state.missed.delete(socket);
|
|
63
|
-
}
|
|
64
|
-
function collectSockets(server) {
|
|
65
|
-
const out = new Set;
|
|
66
|
-
try {
|
|
67
|
-
const channels = server.channels ?? server.clients;
|
|
68
|
-
if (channels && typeof channels.values === "function") {
|
|
69
|
-
for (const set of channels.values())
|
|
70
|
-
if (set && typeof set[Symbol.iterator] === "function")
|
|
71
|
-
for (const entry of set) {
|
|
72
|
-
const ws = entry && typeof entry === "object" && "ws" in entry ? entry.ws : entry;
|
|
73
|
-
if (ws && typeof ws === "object")
|
|
74
|
-
out.add(ws);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
} catch {}
|
|
78
|
-
return [...out];
|
|
79
|
-
}
|
|
80
|
-
function defaultOnDead(socket) {
|
|
81
|
-
const ws = socket;
|
|
82
|
-
if (typeof ws.close === "function")
|
|
83
|
-
ws.close(1011, "heartbeat timeout");
|
|
84
|
-
}
|
|
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")}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
export { getServer, setServer, createServer, stopServer } from "./server-instance";
|
|
3
|
-
export { emit, emitToUser, emitToUsers } from "./emit";
|
|
4
|
-
export { channel, channel as createChannel, Channel as StacksChannel } from "./channel";
|
|
5
|
-
export { broadcast as dispatchBroadcast, runBroadcast, Broadcast as LegacyBroadcast } from "./broadcast";
|
|
6
|
-
export { setBackpressureGuard, getBackpressureGuard } from "./broadcast";
|
|
7
|
-
export { getHeartbeatConfig, markPong, runOneTick, setHeartbeatConfig } from "./heartbeat";
|
|
8
|
-
export { debugSnapshot, getReplayBuffer, pruneExpired, recordBroadcast, replaySince, setReplayBuffer } from "./replay-buffer";
|
|
9
|
-
export { handleWebSocketRequest, storeWebSocketEvent } from "./ws";
|
|
10
|
-
export { setWsAuthenticator, getWsAuthenticator } from "./ws";
|
|
1
|
+
export*from"ts-broadcasting";export{getServer,setServer,createServer,stopServer}from"./server-instance";export{emit,emitToUser,emitToUsers}from"./emit";export{channel,channel as createChannel,Channel as StacksChannel}from"./channel";export{broadcast as dispatchBroadcast,runBroadcast,Broadcast as LegacyBroadcast}from"./broadcast";export{setBackpressureGuard,getBackpressureGuard}from"./broadcast";export{getHeartbeatConfig,markPong,runOneTick,setHeartbeatConfig}from"./heartbeat";export{debugSnapshot,getReplayBuffer,pruneExpired,recordBroadcast,replaySince,setReplayBuffer}from"./replay-buffer";export{handleWebSocketRequest,storeWebSocketEvent}from"./ws";export{setWsAuthenticator,getWsAuthenticator}from"./ws";
|
package/dist/replay-buffer.js
CHANGED
|
@@ -1,81 +1 @@
|
|
|
1
|
-
let registry = null;
|
|
2
|
-
export function setReplayBuffer(cfg) {
|
|
3
|
-
if (!cfg) {
|
|
4
|
-
registry = null;
|
|
5
|
-
return;
|
|
6
|
-
}
|
|
7
|
-
registry = {
|
|
8
|
-
channels: cfg.channels ?? [],
|
|
9
|
-
maxPerChannel: cfg.maxPerChannel ?? 100,
|
|
10
|
-
ttlMs: cfg.ttlMs ?? 300000,
|
|
11
|
-
state: new Map
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
export function getReplayBuffer() {
|
|
15
|
-
return registry;
|
|
16
|
-
}
|
|
17
|
-
function shouldBuffer(channel) {
|
|
18
|
-
if (!registry || registry.channels.length === 0)
|
|
19
|
-
return !1;
|
|
20
|
-
for (const pattern of registry.channels) {
|
|
21
|
-
if (pattern === "*")
|
|
22
|
-
return !0;
|
|
23
|
-
if (pattern === channel)
|
|
24
|
-
return !0;
|
|
25
|
-
if (pattern.endsWith(".*") && channel.startsWith(pattern.slice(0, -1)))
|
|
26
|
-
return !0;
|
|
27
|
-
}
|
|
28
|
-
return !1;
|
|
29
|
-
}
|
|
30
|
-
export function recordBroadcast(channel, event, data) {
|
|
31
|
-
if (!registry || !shouldBuffer(channel))
|
|
32
|
-
return null;
|
|
33
|
-
let state = registry.state.get(channel);
|
|
34
|
-
if (!state) {
|
|
35
|
-
state = { messages: [], nextSeq: 1 };
|
|
36
|
-
registry.state.set(channel, state);
|
|
37
|
-
}
|
|
38
|
-
const msg = {
|
|
39
|
-
seq: state.nextSeq++,
|
|
40
|
-
ts: Date.now(),
|
|
41
|
-
event,
|
|
42
|
-
data
|
|
43
|
-
};
|
|
44
|
-
state.messages.push(msg);
|
|
45
|
-
if (state.messages.length > registry.maxPerChannel)
|
|
46
|
-
state.messages.splice(0, state.messages.length - registry.maxPerChannel);
|
|
47
|
-
return msg.seq;
|
|
48
|
-
}
|
|
49
|
-
export function replaySince(channel, sinceSeq) {
|
|
50
|
-
if (!registry)
|
|
51
|
-
return [];
|
|
52
|
-
const state = registry.state.get(channel);
|
|
53
|
-
if (!state)
|
|
54
|
-
return [];
|
|
55
|
-
const now = Date.now(), ttl = registry.ttlMs;
|
|
56
|
-
while (state.messages.length > 0 && now - state.messages[0].ts > ttl)
|
|
57
|
-
state.messages.shift();
|
|
58
|
-
if (state.messages.length === 0)
|
|
59
|
-
return [];
|
|
60
|
-
return state.messages.filter((m) => m.seq > sinceSeq);
|
|
61
|
-
}
|
|
62
|
-
export function pruneExpired() {
|
|
63
|
-
if (!registry)
|
|
64
|
-
return;
|
|
65
|
-
const now = Date.now(), ttl = registry.ttlMs;
|
|
66
|
-
for (const state of registry.state.values())
|
|
67
|
-
while (state.messages.length > 0 && now - state.messages[0].ts > ttl)
|
|
68
|
-
state.messages.shift();
|
|
69
|
-
}
|
|
70
|
-
export function debugSnapshot() {
|
|
71
|
-
const out = {};
|
|
72
|
-
if (!registry)
|
|
73
|
-
return out;
|
|
74
|
-
for (const [ch, state] of registry.state)
|
|
75
|
-
out[ch] = {
|
|
76
|
-
count: state.messages.length,
|
|
77
|
-
firstSeq: state.messages[0]?.seq ?? null,
|
|
78
|
-
lastSeq: state.messages[state.messages.length - 1]?.seq ?? null
|
|
79
|
-
};
|
|
80
|
-
return out;
|
|
81
|
-
}
|
|
1
|
+
let registry=null;export function setReplayBuffer(cfg){if(!cfg){registry=null;return}registry={channels:cfg.channels??[],maxPerChannel:cfg.maxPerChannel??100,ttlMs:cfg.ttlMs??300000,state:new Map}}export function getReplayBuffer(){return registry}function shouldBuffer(channel){if(!registry||registry.channels.length===0)return!1;for(const pattern of registry.channels){if(pattern==="*")return!0;if(pattern===channel)return!0;if(pattern.endsWith(".*")&&channel.startsWith(pattern.slice(0,-1)))return!0}return!1}export function recordBroadcast(channel,event,data){if(!registry||!shouldBuffer(channel))return null;let state=registry.state.get(channel);if(!state){state={messages:[],nextSeq:1};registry.state.set(channel,state)}const msg={seq:state.nextSeq++,ts:Date.now(),event,data};state.messages.push(msg);if(state.messages.length>registry.maxPerChannel)state.messages.splice(0,state.messages.length-registry.maxPerChannel);return msg.seq}export function replaySince(channel,sinceSeq){if(!registry)return[];const state=registry.state.get(channel);if(!state)return[];const now=Date.now(),ttl=registry.ttlMs;while(state.messages.length>0&&now-state.messages[0].ts>ttl)state.messages.shift();if(state.messages.length===0)return[];return state.messages.filter((m)=>m.seq>sinceSeq)}export function pruneExpired(){if(!registry)return;const now=Date.now(),ttl=registry.ttlMs;for(const state of registry.state.values())while(state.messages.length>0&&now-state.messages[0].ts>ttl)state.messages.shift()}export function debugSnapshot(){const out={};if(!registry)return out;for(const[ch,state]of registry.state)out[ch]={count:state.messages.length,firstSeq:state.messages[0]?.seq??null,lastSeq:state.messages[state.messages.length-1]?.seq??null};return out}
|
package/dist/server-instance.js
CHANGED
|
@@ -1,19 +1 @@
|
|
|
1
|
-
let serverInstance = null
|
|
2
|
-
export function setServer(server) {
|
|
3
|
-
serverInstance = server;
|
|
4
|
-
}
|
|
5
|
-
export function getServer() {
|
|
6
|
-
return serverInstance;
|
|
7
|
-
}
|
|
8
|
-
export async function createServer(config) {
|
|
9
|
-
const server = new (await import("ts-broadcasting")).BroadcastServer(config);
|
|
10
|
-
await server.start();
|
|
11
|
-
setServer(server);
|
|
12
|
-
return server;
|
|
13
|
-
}
|
|
14
|
-
export async function stopServer() {
|
|
15
|
-
if (serverInstance) {
|
|
16
|
-
await serverInstance.stop();
|
|
17
|
-
serverInstance = null;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
1
|
+
let serverInstance=null;export function setServer(server){serverInstance=server}export function getServer(){return serverInstance}export async function createServer(config){const server=new(await import("ts-broadcasting")).BroadcastServer(config);await server.start();setServer(server);return server}export async function stopServer(){if(serverInstance){await serverInstance.stop();serverInstance=null}}
|
package/dist/ws.js
CHANGED
|
@@ -1,28 +1 @@
|
|
|
1
|
-
import { getServer,
|
|
2
|
-
export async function storeWebSocketEvent(_type, _socket, _details) {}
|
|
3
|
-
let wsAuthenticator = null;
|
|
4
|
-
export function setWsAuthenticator(fn) {
|
|
5
|
-
wsAuthenticator = fn;
|
|
6
|
-
}
|
|
7
|
-
export function getWsAuthenticator() {
|
|
8
|
-
return wsAuthenticator;
|
|
9
|
-
}
|
|
10
|
-
export async function handleWebSocketRequest(req, server) {
|
|
11
|
-
if (!getServer())
|
|
12
|
-
return new Response("WebSocket server not initialized", { status: 500 });
|
|
13
|
-
if (wsAuthenticator)
|
|
14
|
-
try {
|
|
15
|
-
const result = await wsAuthenticator(req);
|
|
16
|
-
if (!result.ok)
|
|
17
|
-
return new Response(result.message ?? "Unauthorized", { status: result.status ?? 401 });
|
|
18
|
-
if (server.upgrade(req, result.data ? { data: result.data } : void 0))
|
|
19
|
-
return;
|
|
20
|
-
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
21
|
-
} catch (err) {
|
|
22
|
-
console.error("[realtime] WebSocket authenticator threw:", err);
|
|
23
|
-
return new Response("WebSocket auth error", { status: 500 });
|
|
24
|
-
}
|
|
25
|
-
if (server.upgrade(req))
|
|
26
|
-
return;
|
|
27
|
-
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
28
|
-
}
|
|
1
|
+
import{getServer,setServer}from"./server-instance";export async function storeWebSocketEvent(_type,_socket,_details){}let wsAuthenticator=null;export function setWsAuthenticator(fn){wsAuthenticator=fn}export function getWsAuthenticator(){return wsAuthenticator}export async function handleWebSocketRequest(req,server){if(!getServer())return new Response("WebSocket server not initialized",{status:500});if(wsAuthenticator)try{const result=await wsAuthenticator(req);if(!result.ok)return new Response(result.message??"Unauthorized",{status:result.status??401});if(server.upgrade(req,result.data?{data:result.data}:void 0))return;return new Response("WebSocket upgrade failed",{status:400})}catch(err){console.error("[realtime] WebSocket authenticator threw:",err);return new Response("WebSocket auth error",{status:500})}if(server.upgrade(req))return;return new Response("WebSocket upgrade failed",{status:400})}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/realtime",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.70.
|
|
5
|
+
"version": "0.70.259",
|
|
6
6
|
"description": "The Stacks realtime integration. Built on top of ts-broadcasting.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -34,9 +34,16 @@
|
|
|
34
34
|
"default": "./dist/index.js"
|
|
35
35
|
},
|
|
36
36
|
"./*": {
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
37
|
+
"types": "./dist/*.d.ts",
|
|
38
|
+
"bun": "./dist/*.js",
|
|
39
|
+
"import": "./dist/*.js",
|
|
40
|
+
"default": "./dist/*.js"
|
|
41
|
+
},
|
|
42
|
+
"./*.js": {
|
|
43
|
+
"types": "./dist/*.d.ts",
|
|
44
|
+
"bun": "./dist/*.js",
|
|
45
|
+
"import": "./dist/*.js",
|
|
46
|
+
"default": "./dist/*.js"
|
|
40
47
|
}
|
|
41
48
|
},
|
|
42
49
|
"module": "dist/index.js",
|