@stacksjs/realtime 0.75.452 → 0.75.458

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/channel.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ChannelType } from 'ts-broadcasting';
2
+ export declare function stripPrefix(name: string): string;
2
3
  /**
3
4
  * Create a new channel instance
4
5
  *
package/dist/channel.js CHANGED
@@ -1 +1 @@
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)}
1
+ import{getServer}from"./server-instance";const KNOWN_CHANNEL_PREFIXES=["private-","presence-"];export 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 +1 @@
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}`}server.broadcast(channelName,event,data,options?.exclude)}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)}
1
+ import{stripPrefix}from"./channel";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)channelName=`presence-${stripPrefix(channel)}`;else if(options?.private)channelName=`private-${stripPrefix(channel)}`;server.broadcast(channelName,event,data,options?.exclude)}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)}
@@ -1,3 +1,4 @@
1
+ declare function firstSeq(): number;
1
2
  /**
2
3
  * Install (or replace) the replay-buffer config. Pass `null` to disable
3
4
  * and drop all buffered state. Safe to call multiple times.
@@ -61,6 +62,15 @@ export declare function debugSnapshot(): Record<string, { count: number, firstSe
61
62
  * behind Redis each numbers its own copy, so route replay through a
62
63
  * shared store (Redis Streams, Postgres LISTEN/NOTIFY, etc.) there.
63
64
  *
65
+ * A channel's numbers rise by one per message and keep rising across a
66
+ * restart: each channel starts from the clock (milliseconds times 1000),
67
+ * never below a number this process already issued. They used to start
68
+ * at 1, so after a restart every new message was "older" than the seq a
69
+ * reconnecting client held: `replaySince` skipped them, and a client
70
+ * ignoring repeats dropped them live. When the first replayed seq is not
71
+ * the client's seq plus one, messages were lost: evicted, expired, or
72
+ * sent before a restart.
73
+ *
64
74
  * Memory shape: `Map<channel, RingBuffer<BufferedMessage>>`. Bounded by
65
75
  * `maxPerChannel` (default 100) so a chatty channel can't OOM the
66
76
  * server. Entries past `ttlMs` are evicted lazily on read — apps that
@@ -1 +1 @@
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 matches(pattern,channel){if(pattern==="*"||pattern===channel)return!0;return pattern.endsWith(".*")&&channel.startsWith(pattern.slice(0,-1))}function shouldBuffer(channel){if(!registry||registry.channels.length===0)return!1;const bare=channel.replace(/^(?:private|presence)-/,"");return registry.channels.some((pattern)=>matches(pattern,channel)||matches(pattern,bare))}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}
1
+ let registry=null,lastIssued=0;function firstSeq(){return Math.max(Date.now()*1000,lastIssued+1)}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 matches(pattern,channel){if(pattern==="*"||pattern===channel)return!0;return pattern.endsWith(".*")&&channel.startsWith(pattern.slice(0,-1))}function shouldBuffer(channel){if(!registry||registry.channels.length===0)return!1;const bare=channel.replace(/^(?:private|presence)-/,"");return registry.channels.some((pattern)=>matches(pattern,channel)||matches(pattern,bare))}export function recordBroadcast(channel,event,data){if(!registry||!shouldBuffer(channel))return null;let state=registry.state.get(channel);if(!state){state={messages:[],nextSeq:firstSeq()};registry.state.set(channel,state)}const msg={seq:state.nextSeq++,ts:Date.now(),event,data};lastIssued=Math.max(lastIssued,msg.seq);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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/realtime",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.452",
5
+ "version": "0.75.458",
6
6
  "description": "The Stacks realtime integration. Built on top of ts-broadcasting.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -58,9 +58,9 @@
58
58
  "prepublishOnly": "bun run build"
59
59
  },
60
60
  "dependencies": {
61
- "@stacksjs/logging": "0.75.452",
62
- "@stacksjs/path": "0.75.452",
63
- "ts-broadcasting": "^0.0.11"
61
+ "@stacksjs/logging": "0.75.458",
62
+ "@stacksjs/path": "0.75.458",
63
+ "ts-broadcasting": "^0.0.12"
64
64
  },
65
65
  "devDependencies": {
66
66
  "better-dx": "^0.2.24"