@stacksjs/realtime 0.75.72 → 0.75.74
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.d.ts +0 -26
- package/dist/broadcast.js +1 -1
- package/dist/emit.d.ts +2 -2
- package/dist/emit.js +1 -1
- package/dist/index.d.ts +10 -14
- package/dist/index.js +1 -1
- package/dist/replay-buffer.d.ts +25 -19
- package/dist/replay-buffer.js +1 -1
- package/dist/server-instance.d.ts +22 -2
- package/dist/server-instance.js +1 -1
- package/dist/ws.d.ts +18 -18
- package/dist/ws.js +1 -1
- package/package.json +4 -4
- package/dist/heartbeat.d.ts +0 -32
- package/dist/heartbeat.js +0 -1
package/dist/broadcast.d.ts
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
import type { ChannelType } from 'ts-broadcasting';
|
|
2
|
-
/**
|
|
3
|
-
* Install (or clear) the backpressure guard. Pass `null` to disable.
|
|
4
|
-
*/
|
|
5
|
-
export declare function setBackpressureGuard(cfg: BackpressureGuardConfig | null): void;
|
|
6
|
-
/**
|
|
7
|
-
* Read the currently-installed guard config (useful for tests).
|
|
8
|
-
*/
|
|
9
|
-
export declare function getBackpressureGuard(): Required<BackpressureGuardConfig> | null;
|
|
10
2
|
/**
|
|
11
3
|
* Run a broadcast from a broadcast file
|
|
12
4
|
*
|
|
@@ -21,24 +13,6 @@ export declare function runBroadcast(name: string, payload?: any): Promise<void>
|
|
|
21
13
|
* await broadcast('OrderCreated', { orderId: 123 })
|
|
22
14
|
*/
|
|
23
15
|
export declare function broadcast(name: string, payload?: any): Promise<void>;
|
|
24
|
-
/**
|
|
25
|
-
* Backpressure guard config (stacksjs/stacks#1877 R-2). The default
|
|
26
|
-
* threshold is 1 MiB of buffered-bytes per socket — above this, the
|
|
27
|
-
* configured `onSlow` callback fires once per offending socket per
|
|
28
|
-
* broadcast. Apps install via `setBackpressureGuard({...})`; the
|
|
29
|
-
* default is "no guard" for backwards-compat, so existing callers
|
|
30
|
-
* see no behavior change until they opt in.
|
|
31
|
-
*
|
|
32
|
-
* Why opt-in: the underlying ts-broadcasting `server.broadcast()` is
|
|
33
|
-
* synchronous and we can't inject between message-serialize and
|
|
34
|
-
* socket-write. The best we can do at the Stacks layer is detect
|
|
35
|
-
* slow consumers AROUND the broadcast call and let the app decide
|
|
36
|
-
* what to do (close socket, drop client from channel, scale up).
|
|
37
|
-
*/
|
|
38
|
-
export declare interface BackpressureGuardConfig {
|
|
39
|
-
maxPerSocketBytes?: number
|
|
40
|
-
onSlow?: (info: { channelName: string, backpressure: number, socket: unknown }) => void
|
|
41
|
-
}
|
|
42
16
|
export declare interface BroadcastInstance {
|
|
43
17
|
channel?: () => string | string[]
|
|
44
18
|
broadcastOn?: () => string | string[]
|
package/dist/broadcast.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{log}from"@stacksjs/logging";import{
|
|
1
|
+
import{log}from"@stacksjs/logging";import{getServer}from"./server-instance";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}`;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=[...new bun.Glob("Broadcasts/**/*.ts").scanSync({cwd:appPath(),absolute:!0})]}catch(error){throw Error(`Failed to scan broadcast files: ${error instanceof Error?error.message:String(error)}`)}const root=appPath("Broadcasts"),nameOf=(file)=>file.slice(root.length+1,-3).split("\\").join("/"),exact=broadcastFiles.filter((file)=>nameOf(file)===name),byBasename=name.includes("/")?[]:broadcastFiles.filter((file)=>nameOf(file).split("/").pop()===name),candidates=exact.length>0?exact:byBasename;if(candidates.length>1)throw Error(`Broadcast ${name} is ambiguous: ${candidates.map(nameOf).join(", ")}. Name it by its path under app/Broadcasts.`);const broadcastFile=candidates[0];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/emit.d.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* // Emit to presence channel
|
|
12
12
|
* emit('chat.room.1', 'message', { text: 'Hello' }, { presence: true })
|
|
13
13
|
*
|
|
14
|
-
* //
|
|
15
|
-
* emit('chat.room.1', 'message', { text: 'Hello' }, { exclude: '
|
|
14
|
+
* // Leave out the sender's socket (its socket ID, from the request)
|
|
15
|
+
* emit('chat.room.1', 'message', { text: 'Hello' }, { exclude: request.header('X-Socket-ID') })
|
|
16
16
|
*/
|
|
17
17
|
export declare function emit<T = unknown>(channel: string, event: string, data?: T, options?: EmitOptions): void;
|
|
18
18
|
/**
|
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}`}
|
|
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)}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
export type { EmitOptions } from './emit';
|
|
2
2
|
export type { BroadcastInstance } from './broadcast';
|
|
3
|
-
export type { BackpressureGuardConfig } from './broadcast';
|
|
4
|
-
export type { HeartbeatConfig } from './heartbeat';
|
|
5
3
|
export type { BufferedMessage, ReplayBufferConfig } from './replay-buffer';
|
|
6
4
|
export type { WsAuthenticator, WsAuthResult } from './ws';
|
|
7
5
|
/**
|
|
@@ -20,18 +18,16 @@ export { getServer, setServer, createServer, stopServer } from './server-instanc
|
|
|
20
18
|
export { emit, emitToUser, emitToUsers } from './emit';
|
|
21
19
|
export { channel, channel as createChannel, Channel as StacksChannel } from './channel';
|
|
22
20
|
export { broadcast as dispatchBroadcast, runBroadcast, Broadcast as LegacyBroadcast } from './broadcast';
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
// Heartbeat ping/pong for detecting half-closed sockets
|
|
27
|
-
// (stacksjs/stacks#1877 R-5). Opt-in via setHeartbeatConfig.
|
|
28
|
-
export { getHeartbeatConfig, markPong, runOneTick, setHeartbeatConfig } from './heartbeat';
|
|
21
|
+
// Dead-socket detection and slow-consumer handling are the broadcast
|
|
22
|
+
// server's own: pass `websocket: { idleTimeout, sendPings,
|
|
23
|
+
// backpressureLimit, closeOnBackpressureLimit }` to `createServer()`.
|
|
29
24
|
// At-least-once replay buffer for reconnect (stacksjs/stacks#1877 R-3).
|
|
30
|
-
// Opt-in via setReplayBuffer.
|
|
31
|
-
//
|
|
25
|
+
// Opt-in via setReplayBuffer. Every broadcast on a buffered channel is
|
|
26
|
+
// recorded and carries a top-level `seq`; apps wire `replaySince(channel,
|
|
27
|
+
// seq)` into their reconnect handling to re-send missed messages.
|
|
32
28
|
export { debugSnapshot, getReplayBuffer, pruneExpired, recordBroadcast, replaySince, setReplayBuffer } from './replay-buffer';
|
|
33
|
-
export {
|
|
34
|
-
// WebSocket authenticator
|
|
35
|
-
//
|
|
36
|
-
//
|
|
29
|
+
export { storeWebSocketEvent } from './ws';
|
|
30
|
+
// WebSocket authenticator (stacksjs/stacks#1877 R-1). Gates every upgrade
|
|
31
|
+
// on the server `createServer()` starts; without one, upgrades proceed
|
|
32
|
+
// unauthed.
|
|
37
33
|
export { setWsAuthenticator, getWsAuthenticator } from './ws';
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
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{
|
|
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{debugSnapshot,getReplayBuffer,pruneExpired,recordBroadcast,replaySince,setReplayBuffer}from"./replay-buffer";export{storeWebSocketEvent}from"./ws";export{setWsAuthenticator,getWsAuthenticator}from"./ws";
|
package/dist/replay-buffer.d.ts
CHANGED
|
@@ -6,26 +6,26 @@ export declare function setReplayBuffer(cfg: ReplayBufferConfig | null): void;
|
|
|
6
6
|
/** Read the current config — useful for tests. */
|
|
7
7
|
export declare function getReplayBuffer(): Readonly<BufferRegistry> | null;
|
|
8
8
|
/**
|
|
9
|
-
* Called
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* Called for every outbound broadcast by the hook `setServer()`
|
|
10
|
+
* installs. Records the message under its full channel name (the one
|
|
11
|
+
* clients subscribe to) and assigns a monotonic seq, which the hook
|
|
12
|
+
* stamps onto the outbound frame as `seq`. Returns null for a channel
|
|
13
|
+
* no pattern covers.
|
|
14
14
|
*/
|
|
15
15
|
export declare function recordBroadcast(channel: string, event: string, data: unknown): number | null;
|
|
16
16
|
/**
|
|
17
|
-
* Replay every buffered message on `channel`
|
|
18
|
-
*
|
|
19
|
-
* so callers don't see them.
|
|
20
|
-
* caller should re-send to the
|
|
17
|
+
* Replay every buffered message on `channel` (the full name, e.g.
|
|
18
|
+
* `private-orders.1`) with `seq > sinceSeq`. Stale entries (older than
|
|
19
|
+
* `ttlMs`) are evicted on the way through so callers don't see them.
|
|
20
|
+
* Returns the array of messages the caller should re-send to the
|
|
21
|
+
* reconnecting client.
|
|
21
22
|
*
|
|
22
23
|
* @example
|
|
23
24
|
* ```ts
|
|
24
|
-
* //
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* }
|
|
25
|
+
* // The client kept `message.seq` from the last frame it received on
|
|
26
|
+
* // the channel and sends it back after reconnecting, e.g. to a route:
|
|
27
|
+
* const missed = replaySince('private-orders.1', lastSeenSeq)
|
|
28
|
+
* return missed.map(msg => ({ event: msg.event, channel: 'private-orders.1', data: msg.data, seq: msg.seq }))
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
31
31
|
export declare function replaySince(channel: string, sinceSeq: number): BufferedMessage[];
|
|
@@ -49,11 +49,17 @@ export declare function debugSnapshot(): Record<string, { count: number, firstSe
|
|
|
49
49
|
* updates), reconnect-after-network-blip becomes a silent data loss.
|
|
50
50
|
*
|
|
51
51
|
* Fix: opt-in per-channel ring buffer that retains the most-recent N
|
|
52
|
-
* messages with monotonic sequence IDs.
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
52
|
+
* messages with monotonic sequence IDs. Every broadcast on a buffered
|
|
53
|
+
* channel - whichever API sent it - is recorded through the hook
|
|
54
|
+
* `setServer()` installs on the server, and reaches clients with its
|
|
55
|
+
* sequence number as a top-level field: `{ event, channel, data, seq }`.
|
|
56
|
+
* A client keeps the last `seq` it saw per channel; on reconnect it
|
|
57
|
+
* sends that back (through a route or action of the app's), and the app
|
|
58
|
+
* re-sends `replaySince(channel, seq)`. Apps install via
|
|
59
|
+
* `setReplayBuffer({ channels, maxPerChannel, ttlMs })`. Buffer is
|
|
60
|
+
* in-process, and so are the sequence numbers - with several instances
|
|
61
|
+
* behind Redis each numbers its own copy, so route replay through a
|
|
62
|
+
* shared store (Redis Streams, Postgres LISTEN/NOTIFY, etc.) there.
|
|
57
63
|
*
|
|
58
64
|
* Memory shape: `Map<channel, RingBuffer<BufferedMessage>>`. Bounded by
|
|
59
65
|
* `maxPerChannel` (default 100) so a chatty channel can't OOM the
|
package/dist/replay-buffer.js
CHANGED
|
@@ -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
|
|
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,6 +1,20 @@
|
|
|
1
1
|
import type { BroadcastServer, ServerConfig } from 'ts-broadcasting';
|
|
2
2
|
/**
|
|
3
|
-
* Set the global broadcast server instance
|
|
3
|
+
* Set the global broadcast server instance.
|
|
4
|
+
*
|
|
5
|
+
* ts-broadcasting's own facade is pointed at it too. `@stacksjs/realtime`
|
|
6
|
+
* re-exports ts-broadcasting, so `broadcast()`, `broadcastToUser()` and
|
|
7
|
+
* `Broadcast.send()` from this package are ts-broadcasting's, and they read
|
|
8
|
+
* the server from `Broadcast.setServer()`, which nothing called. Every one of
|
|
9
|
+
* them threw "Broadcast server not initialized" against a running server -
|
|
10
|
+
* including the ORM, which broadcasts model events through `broadcast()` and
|
|
11
|
+
* logs that error, so no model event ever reached a socket.
|
|
12
|
+
*
|
|
13
|
+
* The replay buffer is installed on the server as a broadcast hook, so
|
|
14
|
+
* every broadcast - `emit()`, `channel()`, `runBroadcast()`, the facade, the
|
|
15
|
+
* legacy `Broadcast` class - is recorded and carries its sequence number to
|
|
16
|
+
* clients as a top-level `seq` field. Recording used to happen only in the
|
|
17
|
+
* legacy class, and the number never left the server.
|
|
4
18
|
*/
|
|
5
19
|
export declare function setServer(server: BroadcastServer | null): void;
|
|
6
20
|
/**
|
|
@@ -8,7 +22,13 @@ export declare function setServer(server: BroadcastServer | null): void;
|
|
|
8
22
|
*/
|
|
9
23
|
export declare function getServer(): BroadcastServer | null;
|
|
10
24
|
/**
|
|
11
|
-
* Create and start a new broadcast server
|
|
25
|
+
* Create and start a new broadcast server.
|
|
26
|
+
*
|
|
27
|
+
* This is the WebSocket endpoint Stacks apps serve (`/app` and `/ws` on the
|
|
28
|
+
* configured host/port), and the authenticator installed with
|
|
29
|
+
* `setWsAuthenticator()` now gates it: it used to be consulted only by
|
|
30
|
+
* `handleWebSocketRequest()`, which nothing called, while this server
|
|
31
|
+
* upgraded every request it got.
|
|
12
32
|
*/
|
|
13
33
|
export declare function createServer(config: ServerConfig): Promise<BroadcastServer>;
|
|
14
34
|
/**
|
package/dist/server-instance.js
CHANGED
|
@@ -1 +1 @@
|
|
|
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();
|
|
1
|
+
import{Broadcast}from"ts-broadcasting";import{recordBroadcast}from"./replay-buffer";import{getWsAuthenticator}from"./ws";let serverInstance=null,removeReplayHook=null;export function setServer(server){removeReplayHook?.();removeReplayHook=null;serverInstance=server;Broadcast.setServer(server);if(server)removeReplayHook=server.addBroadcastHook(({channel,event,data})=>{const seq=recordBroadcast(channel,event,data);return seq===null?void 0:{seq}})}export function getServer(){return serverInstance}function stacksConnectionAuthorizer(config){return async(req,user)=>{let accepted={};const authenticate=getWsAuthenticator();if(authenticate){const result=await authenticate(req);if(!result.ok)return{ok:!1,status:result.status,message:result.message};accepted={user:result.user,data:result.data}}const resolvedUser=accepted.user!==void 0?accepted.user:user;if(config.authorizeConnection){const result=await config.authorizeConnection(req,resolvedUser);if(result===!0)return{ok:!0,user:resolvedUser,data:accepted.data};return result}return{ok:!0,user:resolvedUser,data:accepted.data}}}export async function createServer(config){const server=new(await import("ts-broadcasting")).BroadcastServer({...config,authorizeConnection:stacksConnectionAuthorizer(config)});await server.start();setServer(server);return server}export async function stopServer(){if(serverInstance){await serverInstance.stop();setServer(null)}}
|
package/dist/ws.d.ts
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { User } from 'ts-broadcasting';
|
|
2
2
|
/**
|
|
3
3
|
* Store WebSocket event in the database
|
|
4
4
|
* Note: This function is now a no-op. WebSocket events are tracked internally by ts-broadcasting.
|
|
5
5
|
*/
|
|
6
6
|
export declare function storeWebSocketEvent(_type: 'disconnection' | 'error' | 'success', _socket: string, _details: string): Promise<void>;
|
|
7
7
|
/**
|
|
8
|
-
* Install (or clear) the global WebSocket authenticator.
|
|
9
|
-
*
|
|
8
|
+
* Install (or clear) the global WebSocket authenticator. Pass `null`
|
|
9
|
+
* to disable auth (the unauthed default). A refusal answers the
|
|
10
|
+
* upgrade with `status` (default 401) and `message` (default
|
|
11
|
+
* 'Unauthorized'); an authenticator that throws refuses it with a 500
|
|
12
|
+
* and the error is logged, never sent to the client.
|
|
10
13
|
*/
|
|
11
14
|
export declare function setWsAuthenticator(fn: WsAuthenticator | null): void;
|
|
12
15
|
/** Read the currently-installed authenticator. Useful for tests. */
|
|
13
16
|
export declare function getWsAuthenticator(): WsAuthenticator | null;
|
|
14
|
-
/**
|
|
15
|
-
* Handle WebSocket request upgrade. If an authenticator is installed
|
|
16
|
-
* (see `setWsAuthenticator`), it runs FIRST and a 401 is returned on
|
|
17
|
-
* failure (stacksjs/stacks#1877 R-1). Without an authenticator the
|
|
18
|
-
* upgrade proceeds for backwards-compat — the function still works
|
|
19
|
-
* the same way it did before.
|
|
20
|
-
*/
|
|
21
|
-
export declare function handleWebSocketRequest(req: Request, server: Server<any>): Promise<Response | undefined>;
|
|
22
17
|
/**
|
|
23
18
|
* Optional authenticator invoked at WebSocket handshake time.
|
|
24
19
|
*
|
|
25
20
|
* Apps install one via `setWsAuthenticator(fn)` to require a valid
|
|
26
21
|
* token / cookie / signed query param BEFORE the upgrade goes through
|
|
27
|
-
* (stacksjs/stacks#1877 R-1).
|
|
28
|
-
*
|
|
29
|
-
*
|
|
22
|
+
* (stacksjs/stacks#1877 R-1). It gates the broadcast server
|
|
23
|
+
* `createServer()` starts, and is read on every upgrade, so it applies
|
|
24
|
+
* whether it is installed before or after the server starts. Without
|
|
25
|
+
* one, every upgrade proceeds - fine for local dev or public-only
|
|
26
|
+
* broadcasting, but production apps should install one.
|
|
30
27
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
28
|
+
* On success, `user` becomes `socket.data.user` and `data` becomes
|
|
29
|
+
* `socket.data.data` on the upgraded socket, which is what channel
|
|
30
|
+
* authorization callbacks (`Broadcast.channel('private-orders.{id}',
|
|
31
|
+
* (socket, params) => ...)`) read. Channel authorization still decides
|
|
32
|
+
* who may join each private/presence channel; this decides who may
|
|
33
|
+
* connect at all.
|
|
34
34
|
*/
|
|
35
35
|
export type WsAuthenticator = (req: Request) => Promise<WsAuthResult> | WsAuthResult;
|
|
36
36
|
/** Result returned from a `WsAuthenticator`. */
|
|
37
|
-
export type WsAuthResult = | { ok: true, data?: Record<string, unknown> }
|
|
37
|
+
export type WsAuthResult = | { ok: true, user?: User, data?: Record<string, unknown> }
|
|
38
38
|
| { ok: false, status?: number, message?: string }
|
package/dist/ws.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
export async function storeWebSocketEvent(_type,_socket,_details){}let wsAuthenticator=null;export function setWsAuthenticator(fn){wsAuthenticator=fn}export function getWsAuthenticator(){return wsAuthenticator}
|
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.
|
|
5
|
+
"version": "0.75.74",
|
|
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.
|
|
62
|
-
"@stacksjs/path": "0.75.
|
|
63
|
-
"ts-broadcasting": "^0.0.
|
|
61
|
+
"@stacksjs/logging": "0.75.74",
|
|
62
|
+
"@stacksjs/path": "0.75.74",
|
|
63
|
+
"ts-broadcasting": "^0.0.11"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"better-dx": "^0.2.24"
|
package/dist/heartbeat.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Install (or replace) the heartbeat config. Pass `null` to stop
|
|
3
|
-
* the heartbeat loop. Safe to call multiple times — the previous
|
|
4
|
-
* timer is cleared before the new one starts.
|
|
5
|
-
*/
|
|
6
|
-
export declare function setHeartbeatConfig(cfg: HeartbeatConfig | null): void;
|
|
7
|
-
/** Read the current config — useful for tests. */
|
|
8
|
-
export declare function getHeartbeatConfig(): Readonly<HeartbeatState> | null;
|
|
9
|
-
/**
|
|
10
|
-
* Manually fire a single heartbeat tick. Exposed for tests; in
|
|
11
|
-
* production it's invoked by the internal interval.
|
|
12
|
-
*/
|
|
13
|
-
export declare function runOneTick(): void;
|
|
14
|
-
/**
|
|
15
|
-
* Called from the server's pong handler (or message handler when
|
|
16
|
-
* fallback `__stacks_ping__` text frames are in use). Resets the
|
|
17
|
-
* missed-pong counter for the given socket so it doesn't get
|
|
18
|
-
* declared dead.
|
|
19
|
-
*/
|
|
20
|
-
export declare function markPong(socket: object): void;
|
|
21
|
-
export declare interface HeartbeatConfig {
|
|
22
|
-
intervalMs?: number
|
|
23
|
-
maxMissedPongs?: number
|
|
24
|
-
onDead?: (socket: unknown) => void
|
|
25
|
-
}
|
|
26
|
-
declare interface HeartbeatState {
|
|
27
|
-
intervalMs: number
|
|
28
|
-
maxMissedPongs: number
|
|
29
|
-
onDead: (socket: unknown) => void
|
|
30
|
-
missed: WeakMap<object, number>
|
|
31
|
-
timer: ReturnType<typeof setInterval> | null
|
|
32
|
-
}
|
package/dist/heartbeat.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
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")}
|