redweb 0.8.0 → 0.9.0
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/CHANGELOG.md +11 -0
- package/README.md +458 -307
- package/client.d.ts +42 -0
- package/client.js +55 -0
- package/docs/MULTIPLAYER_OPERATIONS.md +50 -0
- package/docs/PRODUCTION_READINESS.md +68 -0
- package/docs/VERIFICATION_EVIDENCE.md +20 -0
- package/index.d.ts +320 -114
- package/index.js +27 -12
- package/package.json +28 -15
- package/src/htmx/HtmxRenderer.js +13 -13
- package/src/http/BaseHttpServer.js +112 -112
- package/src/http/HttpServer.js +18 -18
- package/src/http/HttpsServer.js +20 -20
- package/src/serverLifecycle.js +46 -46
- package/src/ws/AdmissionPolicy.js +145 -0
- package/src/ws/BaseHandler.js +40 -40
- package/src/ws/BaseSocketServer.js +195 -100
- package/src/ws/DefaultHandler.js +5 -5
- package/src/ws/DefaultRoute.js +8 -8
- package/src/ws/DistributionBridge.js +271 -0
- package/src/ws/FixedStepService.js +74 -0
- package/src/ws/HeartbeatMonitor.js +75 -0
- package/src/ws/Metrics.js +34 -0
- package/src/ws/ProtocolPolicy.js +130 -0
- package/src/ws/RoomRegistry.js +117 -0
- package/src/ws/RouteRuntime.js +146 -0
- package/src/ws/SecureSocketServer.js +9 -9
- package/src/ws/SessionRegistry.js +135 -0
- package/src/ws/SocketRoute.js +523 -254
- package/src/ws/SocketServer.js +8 -8
- package/src/ws/TaskQueue.js +64 -0
- package/src/ws/TokenBucket.js +31 -0
- package/src/ws/TransportPolicy.js +68 -0
- package/src/ws/index.js +7 -2
- package/src/ws/protocol-schema.json +13 -0
- package/src/ws/protocol-validation.js +21 -0
- package/src/ws/shutdown.js +33 -33
- package/src/ws/util.js +38 -30
package/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
declare module 'redweb' {
|
|
2
2
|
import { Application } from 'express';
|
|
3
3
|
import { CorsOptions } from 'cors';
|
|
4
|
-
import { Server as NodeHttpServer } from 'http';
|
|
5
|
-
import { WebSocket, ServerOptions } from 'ws';
|
|
6
|
-
import { Buffer } from 'buffer';
|
|
7
|
-
import { EventEmitter } from 'events';
|
|
4
|
+
import { Server as NodeHttpServer } from 'http';
|
|
5
|
+
import { WebSocket, ServerOptions } from 'ws';
|
|
6
|
+
import { Buffer } from 'buffer';
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
8
|
|
|
9
9
|
/** ─────────────────── HTTP / CORE ─────────────────── */
|
|
10
10
|
|
|
@@ -13,70 +13,217 @@ declare module 'redweb' {
|
|
|
13
13
|
export interface RedWebOptions {
|
|
14
14
|
port?: number;
|
|
15
15
|
bind?: string;
|
|
16
|
-
publicPaths?: string[];
|
|
17
|
-
services?: Array<{ serviceName: string; method: string; function: Function }>;
|
|
18
|
-
listen?: boolean;
|
|
19
|
-
listenCallback?: () => void;
|
|
16
|
+
publicPaths?: string[];
|
|
17
|
+
services?: Array<{ serviceName: string; method: string; function: Function }>;
|
|
18
|
+
listen?: boolean;
|
|
19
|
+
listenCallback?: () => void;
|
|
20
20
|
encoding?: RedWebEncoding;
|
|
21
21
|
ssl?: { key: string; cert: string };
|
|
22
22
|
server?: Application;
|
|
23
|
-
corsOptions?: CorsOptions | false;
|
|
24
|
-
enableHtmxRendering?: boolean;
|
|
25
|
-
exposeErrors?: boolean;
|
|
26
|
-
logger?: RedWebLogger | null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type RedWebSocket = WebSocket & {
|
|
30
|
-
clientKey: string;
|
|
31
|
-
__redwebClientKey: string;
|
|
32
|
-
remoteAddress: string;
|
|
33
|
-
isAssigned: boolean;
|
|
34
|
-
sendJson(data: unknown): boolean;
|
|
35
|
-
broadcast(data: unknown): number;
|
|
36
|
-
|
|
23
|
+
corsOptions?: CorsOptions | false;
|
|
24
|
+
enableHtmxRendering?: boolean;
|
|
25
|
+
exposeErrors?: boolean;
|
|
26
|
+
logger?: RedWebLogger | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type RedWebSocket = WebSocket & {
|
|
30
|
+
clientKey: string;
|
|
31
|
+
__redwebClientKey: string;
|
|
32
|
+
remoteAddress: string;
|
|
33
|
+
isAssigned: boolean;
|
|
34
|
+
sendJson(data: unknown): boolean;
|
|
35
|
+
broadcast(data: unknown): number;
|
|
36
|
+
context?: RedWebConnectionContext;
|
|
37
|
+
joinRoom?(roomId: string): boolean;
|
|
38
|
+
leaveRoom?(roomId: string): boolean;
|
|
39
|
+
roomBroadcast?(roomId: string, data: unknown, options?: { except?: RedWebSocket }): number;
|
|
40
|
+
createSession?(sessionId: string, data: unknown): boolean;
|
|
41
|
+
resumeSession?(sessionId: string): unknown | null;
|
|
42
|
+
publishEvent?(type: string, payload: unknown): Promise<boolean>;
|
|
43
|
+
sendEvent?(type: string, payload: unknown, metadata?: ProtocolMetadata): boolean;
|
|
44
|
+
sendProtocolError?(code: string, message: string, metadata?: ProtocolMetadata): boolean;
|
|
45
|
+
sendBinaryEvent?(value: unknown): Promise<boolean>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export interface RedWebConnectionContext {
|
|
49
|
+
connectionId: string;
|
|
50
|
+
principal: unknown;
|
|
51
|
+
session: unknown | null;
|
|
52
|
+
metadata: Record<string, unknown>;
|
|
53
|
+
signal?: AbortSignal;
|
|
54
|
+
protocol?: Readonly<{ version: string }>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AdmissionContext {
|
|
58
|
+
signal: AbortSignal;
|
|
59
|
+
networkIdentity: string;
|
|
60
|
+
route: SocketRoute;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface AdmissionOptions {
|
|
64
|
+
authenticate?: (
|
|
65
|
+
request: import('http').IncomingMessage,
|
|
66
|
+
context: AdmissionContext
|
|
67
|
+
) => unknown | false | Promise<unknown | false>;
|
|
68
|
+
origins?: string[] | ((
|
|
69
|
+
origin: string | undefined,
|
|
70
|
+
request: import('http').IncomingMessage
|
|
71
|
+
) => boolean | Promise<boolean>);
|
|
72
|
+
place?: (
|
|
73
|
+
principal: unknown,
|
|
74
|
+
request: import('http').IncomingMessage,
|
|
75
|
+
context: AdmissionContext
|
|
76
|
+
) => string | false | null | undefined | Promise<string | false | null | undefined>;
|
|
77
|
+
allowedPlacementOrigins?: string[];
|
|
78
|
+
allowInsecurePlacement?: boolean;
|
|
79
|
+
timeoutMs?: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface MessageRateLimit {
|
|
83
|
+
capacity: number;
|
|
84
|
+
refillPerSecond: number;
|
|
85
|
+
action?: 'drop' | 'disconnect';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface TransportLimits {
|
|
89
|
+
maxConnections?: number;
|
|
90
|
+
maxBufferedBytes?: number;
|
|
91
|
+
maxPendingMessages?: number;
|
|
92
|
+
messageRate?: MessageRateLimit;
|
|
93
|
+
slowConsumerAction?: 'drop' | 'disconnect';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface HeartbeatOptions {
|
|
97
|
+
intervalMs: number;
|
|
98
|
+
timeoutMs: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface RoomOptions {
|
|
102
|
+
maxRooms?: number;
|
|
103
|
+
maxMembersPerRoom?: number;
|
|
104
|
+
maxRoomsPerConnection?: number;
|
|
105
|
+
maxRoomIdLength?: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface SessionOptions {
|
|
109
|
+
ttlMs?: number;
|
|
110
|
+
maxSessions?: number;
|
|
111
|
+
maxSessionIdLength?: number;
|
|
112
|
+
sweepIntervalMs?: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface MetricsSink {
|
|
116
|
+
increment?(name: string, value: number, attributes: Readonly<{ route: string }>): void | Promise<void>;
|
|
117
|
+
gauge?(name: string, value: number, attributes: Readonly<{ route: string }>): void | Promise<void>;
|
|
118
|
+
observe?(name: string, value: number, attributes: Readonly<{ route: string }>): void | Promise<void>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface DistributionEvent<T = unknown> {
|
|
122
|
+
id: string;
|
|
123
|
+
source: string;
|
|
124
|
+
type: string;
|
|
125
|
+
payload: T;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface DistributionAdapter {
|
|
129
|
+
start?(signal?: AbortSignal): void | Promise<void>;
|
|
130
|
+
publish(channel: string, serializedEvent: string, signal?: AbortSignal): void | Promise<void>;
|
|
131
|
+
subscribe(
|
|
132
|
+
channel: string,
|
|
133
|
+
onEvent: (serializedEvent: string | DistributionEvent) => void,
|
|
134
|
+
signal?: AbortSignal
|
|
135
|
+
): void | (() => void | Promise<void>) | Promise<void | (() => void | Promise<void>)>;
|
|
136
|
+
unsubscribe?(channel: string, signal?: AbortSignal): void | Promise<void>;
|
|
137
|
+
close?(signal?: AbortSignal): void | Promise<void>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface DistributionOptions {
|
|
141
|
+
adapter: DistributionAdapter;
|
|
142
|
+
channel: string;
|
|
143
|
+
nodeId?: string;
|
|
144
|
+
maxEventBytes?: number;
|
|
145
|
+
maxSeenEvents?: number;
|
|
146
|
+
seenTtlMs?: number;
|
|
147
|
+
lifecycleTimeoutMs?: number;
|
|
148
|
+
publishTimeoutMs?: number;
|
|
149
|
+
maxConcurrentPublishes?: number;
|
|
150
|
+
maxConcurrentEvents?: number;
|
|
151
|
+
required?: boolean;
|
|
152
|
+
onEvent(event: DistributionEvent, route: SocketRoute): void | Promise<void>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface ProtocolMetadata {
|
|
156
|
+
requestId?: string;
|
|
157
|
+
sequence?: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface ProtocolBinaryCodec {
|
|
161
|
+
maxBytes?: number;
|
|
162
|
+
encode(value: unknown, context: RedWebConnectionContext): Buffer | Uint8Array | ArrayBuffer | Promise<Buffer | Uint8Array | ArrayBuffer>;
|
|
163
|
+
decode(buffer: Buffer, context: RedWebConnectionContext): unknown | Promise<unknown>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface ProtocolOptions {
|
|
167
|
+
versions: string[];
|
|
168
|
+
required?: boolean;
|
|
169
|
+
queryParameter?: string;
|
|
170
|
+
header?: string;
|
|
171
|
+
binary?: false | ProtocolBinaryCodec;
|
|
172
|
+
}
|
|
37
173
|
|
|
38
174
|
/** ─────────────────── SOCKET SERVER ─────────────────── */
|
|
39
175
|
|
|
40
176
|
export interface SocketServerOptions {
|
|
41
|
-
server?: NodeHttpServer;
|
|
42
|
-
port?: number;
|
|
43
|
-
bind?: string;
|
|
44
|
-
listen?: boolean;
|
|
45
|
-
routes?: Array<new () => SocketRoute>;
|
|
46
|
-
ssl?: { key: string; cert: string };
|
|
47
|
-
fallbackToRoot?: boolean;
|
|
48
|
-
closeServerOnShutdown?: boolean;
|
|
49
|
-
listenCallback?: () => void;
|
|
50
|
-
logger?: RedWebLogger | null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface RedWebLogger {
|
|
54
|
-
log?(message?: any, ...optionalParams: any[]): void;
|
|
55
|
-
warn?(message?: any, ...optionalParams: any[]): void;
|
|
56
|
-
error?(message?: any, ...optionalParams: any[]): void;
|
|
57
|
-
}
|
|
177
|
+
server?: NodeHttpServer;
|
|
178
|
+
port?: number;
|
|
179
|
+
bind?: string;
|
|
180
|
+
listen?: boolean;
|
|
181
|
+
routes?: Array<new () => SocketRoute>;
|
|
182
|
+
ssl?: { key: string; cert: string };
|
|
183
|
+
fallbackToRoot?: boolean;
|
|
184
|
+
closeServerOnShutdown?: boolean;
|
|
185
|
+
listenCallback?: () => void;
|
|
186
|
+
logger?: RedWebLogger | null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface RedWebLogger {
|
|
190
|
+
log?(message?: any, ...optionalParams: any[]): void;
|
|
191
|
+
warn?(message?: any, ...optionalParams: any[]): void;
|
|
192
|
+
error?(message?: any, ...optionalParams: any[]): void;
|
|
193
|
+
}
|
|
58
194
|
|
|
59
195
|
/** ─────────────────── ROUTES & HANDLERS ─────────────────── */
|
|
60
196
|
|
|
61
197
|
export interface SocketRouteConfig {
|
|
62
198
|
path: string;
|
|
63
|
-
handlers: Array<new () => BaseHandler>;
|
|
64
|
-
services?: Array<new () => SocketService>;
|
|
65
|
-
allowDuplicateConnections?: boolean;
|
|
66
|
-
websocketOptions?: Omit<ServerOptions, 'noServer' | 'path' | 'server' | 'port'>;
|
|
67
|
-
trustProxy?: boolean;
|
|
68
|
-
getClientKey?: (request: import('http').IncomingMessage) => string;
|
|
69
|
-
exposeErrors?: boolean;
|
|
70
|
-
logger?: RedWebLogger | null;
|
|
71
|
-
shutdownTimeoutMs?: number;
|
|
199
|
+
handlers: Array<new () => BaseHandler>;
|
|
200
|
+
services?: Array<new () => SocketService>;
|
|
201
|
+
allowDuplicateConnections?: boolean;
|
|
202
|
+
websocketOptions?: Omit<ServerOptions, 'noServer' | 'path' | 'server' | 'port'>;
|
|
203
|
+
trustProxy?: boolean;
|
|
204
|
+
getClientKey?: (request: import('http').IncomingMessage) => string;
|
|
205
|
+
exposeErrors?: boolean;
|
|
206
|
+
logger?: RedWebLogger | null;
|
|
207
|
+
shutdownTimeoutMs?: number;
|
|
208
|
+
admission?: AdmissionOptions | AdmissionOptions['authenticate'];
|
|
209
|
+
limits?: TransportLimits;
|
|
210
|
+
orderedMessages?: boolean;
|
|
211
|
+
heartbeat?: HeartbeatOptions;
|
|
212
|
+
rooms?: boolean | RoomOptions;
|
|
213
|
+
sessions?: boolean | SessionOptions;
|
|
214
|
+
metrics?: MetricsSink;
|
|
215
|
+
distribution?: false | DistributionOptions;
|
|
216
|
+
drainHandlers?: boolean;
|
|
217
|
+
protocol?: false | ProtocolOptions;
|
|
218
|
+
maxPendingUpgrades?: number;
|
|
72
219
|
}
|
|
73
220
|
|
|
74
221
|
/** Socket‑side autonomous service (game loops, timers, etc.) */
|
|
75
|
-
export abstract class SocketService {
|
|
76
|
-
name: string;
|
|
77
|
-
tickRateMs: number | null;
|
|
78
|
-
route: SocketRoute;
|
|
79
|
-
protected _tickHandle: NodeJS.Timeout | null;
|
|
222
|
+
export abstract class SocketService {
|
|
223
|
+
name: string;
|
|
224
|
+
tickRateMs: number | null;
|
|
225
|
+
route: SocketRoute;
|
|
226
|
+
protected _tickHandle: NodeJS.Timeout | null;
|
|
80
227
|
|
|
81
228
|
constructor(name: string, tickRateMs?: number);
|
|
82
229
|
|
|
@@ -84,68 +231,116 @@ declare module 'redweb' {
|
|
|
84
231
|
onInit(route: SocketRoute): void;
|
|
85
232
|
|
|
86
233
|
/** Optional recurring tick (respecting tickRateMs) */
|
|
87
|
-
onTick?():
|
|
234
|
+
onTick?(...args: any[]): unknown;
|
|
88
235
|
|
|
89
236
|
/** Called on process shutdown / route removal */
|
|
90
237
|
onShutdown(): void;
|
|
91
238
|
}
|
|
92
239
|
|
|
240
|
+
export abstract class FixedStepService extends SocketService {
|
|
241
|
+
maxCatchUpTicks: number;
|
|
242
|
+
maxRetainedLagMs: number;
|
|
243
|
+
tick: number;
|
|
244
|
+
accumulatorMs: number;
|
|
245
|
+
|
|
246
|
+
constructor(name: string, tickRateMs: number, maxCatchUpTicks?: number, maxRetainedLagMs?: number);
|
|
247
|
+
onTick?(stepMs: number, tick: number): void | Promise<void>;
|
|
248
|
+
onLagDropped?(droppedLagMs: number): void;
|
|
249
|
+
pulse(): Promise<void>;
|
|
250
|
+
onShutdown(): Promise<void>;
|
|
251
|
+
}
|
|
252
|
+
|
|
93
253
|
/** Message handler, triggered by client messages */
|
|
94
254
|
export class BaseHandler {
|
|
95
255
|
name: string;
|
|
96
256
|
constructor(name: string);
|
|
97
257
|
|
|
98
258
|
handleMessage(
|
|
99
|
-
socket: RedWebSocket,
|
|
259
|
+
socket: RedWebSocket,
|
|
100
260
|
message: any
|
|
101
|
-
): Promise<unknown>;
|
|
102
|
-
|
|
103
|
-
validateMessage(message: any, socket: RedWebSocket): boolean | Promise<boolean>;
|
|
104
|
-
onMessage(socket: RedWebSocket, message: any): unknown;
|
|
105
|
-
acceptsBinary?(socket: RedWebSocket, buffer: Buffer): boolean;
|
|
106
|
-
handleBinaryMessage(socket: RedWebSocket, buffer: Buffer): Promise<unknown>;
|
|
107
|
-
onBinaryMessage(socket: RedWebSocket, buffer: Buffer): unknown;
|
|
108
|
-
onInitialContact(socket: RedWebSocket, request?: import('http').IncomingMessage): unknown;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export class SocketRoute {
|
|
261
|
+
): Promise<unknown>;
|
|
262
|
+
|
|
263
|
+
validateMessage(message: any, socket: RedWebSocket): boolean | Promise<boolean>;
|
|
264
|
+
onMessage(socket: RedWebSocket, message: any): unknown;
|
|
265
|
+
acceptsBinary?(socket: RedWebSocket, buffer: Buffer): boolean;
|
|
266
|
+
handleBinaryMessage(socket: RedWebSocket, buffer: Buffer): Promise<unknown>;
|
|
267
|
+
onBinaryMessage(socket: RedWebSocket, buffer: Buffer): unknown;
|
|
268
|
+
onInitialContact(socket: RedWebSocket, request?: import('http').IncomingMessage): unknown;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export class SocketRoute {
|
|
112
272
|
path: string;
|
|
113
|
-
handlers: BaseHandler[];
|
|
114
|
-
services: SocketService[];
|
|
115
|
-
clients: Map<string, RedWebSocket>;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
273
|
+
handlers: BaseHandler[];
|
|
274
|
+
services: SocketService[];
|
|
275
|
+
clients: Map<string, RedWebSocket>;
|
|
276
|
+
rooms: RoomRegistry | null;
|
|
277
|
+
sessions: SessionRegistry | null;
|
|
278
|
+
distribution: unknown | null;
|
|
279
|
+
protocolPolicy: unknown | null;
|
|
280
|
+
draining: boolean;
|
|
281
|
+
allowDuplicateConnections?: boolean;
|
|
282
|
+
websocketOptions?: SocketRouteConfig['websocketOptions'];
|
|
283
|
+
|
|
284
|
+
constructor(config: SocketRouteConfig);
|
|
285
|
+
|
|
286
|
+
addHandler(handler: new () => BaseHandler): boolean;
|
|
287
|
+
resolveRemoteAddress(request: import('http').IncomingMessage): string;
|
|
288
|
+
connectionOpenCallback(socket: RedWebSocket, request?: import('http').IncomingMessage): unknown;
|
|
289
|
+
connectionCloseCallback?(socket: RedWebSocket): unknown;
|
|
290
|
+
handleMessage(sock: RedWebSocket, data: any): Promise<boolean>;
|
|
291
|
+
handleBinaryMessage(socket: RedWebSocket, buffer: Buffer): Promise<boolean>;
|
|
292
|
+
beginDrain(): boolean;
|
|
293
|
+
isReady(): boolean;
|
|
294
|
+
publish(type: string, payload: unknown): Promise<boolean>;
|
|
295
|
+
shutdown(): Promise<void>;
|
|
296
|
+
}
|
|
129
297
|
|
|
130
298
|
/** ─────────────────── SERVER BASE ─────────────────── */
|
|
131
299
|
|
|
132
300
|
export class BaseSocketServer {
|
|
133
|
-
server: NodeHttpServer;
|
|
134
|
-
routes: SocketRoute[];
|
|
135
|
-
ownsServer: boolean;
|
|
301
|
+
server: NodeHttpServer;
|
|
302
|
+
routes: SocketRoute[];
|
|
303
|
+
ownsServer: boolean;
|
|
304
|
+
|
|
305
|
+
constructor(server: NodeHttpServer, options?: SocketServerOptions);
|
|
136
306
|
|
|
137
|
-
|
|
307
|
+
addRoute(route: new () => SocketRoute): SocketRoute;
|
|
308
|
+
isReady(): boolean;
|
|
309
|
+
beginDrain(): boolean;
|
|
310
|
+
shutdown(): Promise<void>;
|
|
311
|
+
}
|
|
138
312
|
|
|
139
|
-
|
|
140
|
-
|
|
313
|
+
/** ─────────────────── REGISTRY & UTIL TYPES ─────────────────── */
|
|
314
|
+
|
|
315
|
+
export function sendJson(socket: WebSocket, data: unknown): boolean;
|
|
316
|
+
|
|
317
|
+
export class RoomRegistry {
|
|
318
|
+
constructor(options?: RoomOptions);
|
|
319
|
+
join(roomId: string, socket: RedWebSocket): boolean;
|
|
320
|
+
leave(roomId: string, socket: RedWebSocket): boolean;
|
|
321
|
+
leaveAll(socket: RedWebSocket): number;
|
|
322
|
+
members(roomId: string): RedWebSocket[];
|
|
323
|
+
has(roomId: string, socket: RedWebSocket): boolean;
|
|
324
|
+
broadcast(roomId: string, data: unknown, options?: { except?: RedWebSocket }): number;
|
|
325
|
+
clear(): void;
|
|
326
|
+
close(): boolean;
|
|
327
|
+
readonly size: number;
|
|
141
328
|
}
|
|
142
329
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
330
|
+
export class SessionRegistry<T = unknown> {
|
|
331
|
+
constructor(options?: SessionOptions, logger?: RedWebLogger | null);
|
|
332
|
+
create(sessionId: string, data: T, socket?: RedWebSocket): boolean;
|
|
333
|
+
resume(sessionId: string, socket: RedWebSocket): T | null;
|
|
334
|
+
release(socket: RedWebSocket): boolean;
|
|
335
|
+
remove(sessionId: string): boolean;
|
|
336
|
+
get(sessionId: string): T | undefined;
|
|
337
|
+
sweep(): void;
|
|
338
|
+
stop(): void;
|
|
339
|
+
readonly size: number;
|
|
340
|
+
}
|
|
146
341
|
|
|
147
342
|
export interface SocketWrapper {
|
|
148
|
-
socket: RedWebSocket;
|
|
343
|
+
socket: RedWebSocket;
|
|
149
344
|
id: string;
|
|
150
345
|
send: (type: string, payload: Record<string, any>) => void;
|
|
151
346
|
getSanitized?(): Record<string, any>;
|
|
@@ -190,34 +385,34 @@ declare module 'redweb' {
|
|
|
190
385
|
constructor(options?: SocketServerOptions);
|
|
191
386
|
}
|
|
192
387
|
|
|
193
|
-
export class BaseHttpServer {
|
|
194
|
-
app: Application;
|
|
195
|
-
server?: NodeHttpServer;
|
|
196
|
-
constructor(options?: RedWebOptions);
|
|
197
|
-
shutdown?(): Promise<void>;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export class HttpServer extends BaseHttpServer {
|
|
201
|
-
constructor(options?: RedWebOptions);
|
|
202
|
-
shutdown(): Promise<void>;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export class HttpsServer extends BaseHttpServer {
|
|
206
|
-
constructor(options?: RedWebOptions);
|
|
207
|
-
shutdown(): Promise<void>;
|
|
208
|
-
}
|
|
388
|
+
export class BaseHttpServer {
|
|
389
|
+
app: Application;
|
|
390
|
+
server?: NodeHttpServer;
|
|
391
|
+
constructor(options?: RedWebOptions);
|
|
392
|
+
shutdown?(): Promise<void>;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export class HttpServer extends BaseHttpServer {
|
|
396
|
+
constructor(options?: RedWebOptions);
|
|
397
|
+
shutdown(): Promise<void>;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export class HttpsServer extends BaseHttpServer {
|
|
401
|
+
constructor(options?: RedWebOptions);
|
|
402
|
+
shutdown(): Promise<void>;
|
|
403
|
+
}
|
|
209
404
|
|
|
210
405
|
/** ─────────────────── CONSTANTS ─────────────────── */
|
|
211
406
|
|
|
212
407
|
export const METHODS: {
|
|
213
408
|
GET: 'get';
|
|
214
409
|
POST: 'post';
|
|
215
|
-
PUT: 'put';
|
|
216
|
-
PATCH: 'patch';
|
|
217
|
-
DELETE: 'delete';
|
|
218
|
-
OPTIONS: 'options';
|
|
219
|
-
HEAD: 'head';
|
|
220
|
-
ALL: 'all';
|
|
410
|
+
PUT: 'put';
|
|
411
|
+
PATCH: 'patch';
|
|
412
|
+
DELETE: 'delete';
|
|
413
|
+
OPTIONS: 'options';
|
|
414
|
+
HEAD: 'head';
|
|
415
|
+
ALL: 'all';
|
|
221
416
|
};
|
|
222
417
|
|
|
223
418
|
export const ENCODINGS: {
|
|
@@ -227,4 +422,15 @@ declare module 'redweb' {
|
|
|
227
422
|
|
|
228
423
|
export const HTTP_OPTIONS: RedWebOptions;
|
|
229
424
|
export const SOCKET_OPTIONS: SocketServerOptions;
|
|
425
|
+
|
|
426
|
+
export const ERROR_CODES: Readonly<{
|
|
427
|
+
INVALID_MESSAGE: 'INVALID_MESSAGE';
|
|
428
|
+
UNKNOWN_HANDLER: 'UNKNOWN_HANDLER';
|
|
429
|
+
HANDLER_FAILED: 'HANDLER_FAILED';
|
|
430
|
+
BINARY_UNSUPPORTED: 'BINARY_UNSUPPORTED';
|
|
431
|
+
RATE_LIMITED: 'RATE_LIMITED';
|
|
432
|
+
QUEUE_FULL: 'QUEUE_FULL';
|
|
433
|
+
CAPACITY_REACHED: 'CAPACITY_REACHED';
|
|
434
|
+
INITIALIZATION_FAILED: 'INITIALIZATION_FAILED';
|
|
435
|
+
}>;
|
|
230
436
|
}
|
package/index.js
CHANGED
|
@@ -1,23 +1,38 @@
|
|
|
1
|
-
const { BaseHttpServer, METHODS } = require('./src/http');
|
|
2
|
-
const { ENCODINGS, HTTP_OPTIONS } = require('./src/http/BaseHttpServer');
|
|
1
|
+
const { BaseHttpServer, METHODS } = require('./src/http');
|
|
2
|
+
const { ENCODINGS, HTTP_OPTIONS } = require('./src/http/BaseHttpServer');
|
|
3
3
|
const { sendJson } = require('./src/ws/util');
|
|
4
|
-
const {
|
|
4
|
+
const {
|
|
5
|
+
SocketServer,
|
|
6
|
+
SecureSocketServer,
|
|
7
|
+
SOCKET_OPTIONS,
|
|
8
|
+
SocketRoute,
|
|
9
|
+
SocketService,
|
|
10
|
+
FixedStepService,
|
|
11
|
+
SocketRegistry,
|
|
12
|
+
RoomRegistry,
|
|
13
|
+
SessionRegistry,
|
|
14
|
+
ERROR_CODES,
|
|
15
|
+
} = require('./src/ws');
|
|
5
16
|
const { BaseHandler } = require('./src/ws/BaseHandler');
|
|
6
17
|
const HttpServer = require('./src/http/HttpServer');
|
|
7
18
|
const HttpsServer = require('./src/http/HttpsServer');
|
|
8
19
|
module.exports = {
|
|
9
|
-
HttpServer,
|
|
10
|
-
HttpsServer,
|
|
11
|
-
BaseHttpServer,
|
|
12
|
-
SocketServer,
|
|
20
|
+
HttpServer,
|
|
21
|
+
HttpsServer,
|
|
22
|
+
BaseHttpServer,
|
|
23
|
+
SocketServer,
|
|
13
24
|
SecureSocketServer,
|
|
14
25
|
BaseHandler,
|
|
15
26
|
SocketRoute,
|
|
16
27
|
SocketService,
|
|
28
|
+
FixedStepService,
|
|
17
29
|
SocketRegistry,
|
|
30
|
+
RoomRegistry,
|
|
31
|
+
SessionRegistry,
|
|
32
|
+
ERROR_CODES,
|
|
18
33
|
sendJson,
|
|
19
|
-
SOCKET_OPTIONS,
|
|
20
|
-
HTTP_OPTIONS,
|
|
21
|
-
ENCODINGS,
|
|
22
|
-
METHODS
|
|
23
|
-
};
|
|
34
|
+
SOCKET_OPTIONS,
|
|
35
|
+
HTTP_OPTIONS,
|
|
36
|
+
ENCODINGS,
|
|
37
|
+
METHODS
|
|
38
|
+
};
|
package/package.json
CHANGED
|
@@ -1,31 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "redweb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "A way to quickly set up an express server",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"pretest": "tsc -p tests/types/tsconfig.json",
|
|
9
|
-
"
|
|
7
|
+
"scripts": {
|
|
8
|
+
"pretest": "node scripts/generate-protocol-types.js --check && tsc -p tests/types/tsconfig.json",
|
|
9
|
+
"generate:protocol-types": "node scripts/generate-protocol-types.js",
|
|
10
|
+
"verify:overhead": "node scripts/verify-disabled-overhead.js",
|
|
11
|
+
"verify:soak": "node --expose-gc scripts/verify-soak.js",
|
|
12
|
+
"verify:memory": "node scripts/verify-memory-overhead.js",
|
|
13
|
+
"verify:load": "node scripts/verify-load.js",
|
|
14
|
+
"verify:recovery": "node --expose-gc scripts/verify-recovery.js",
|
|
15
|
+
"test": "npx jest"
|
|
10
16
|
},
|
|
11
17
|
"files": [
|
|
12
18
|
"src/*",
|
|
13
|
-
"
|
|
19
|
+
"client.js",
|
|
20
|
+
"client.d.ts",
|
|
21
|
+
"CHANGELOG.md",
|
|
22
|
+
"index.d.ts",
|
|
23
|
+
"docs"
|
|
14
24
|
],
|
|
15
25
|
"keywords": [],
|
|
16
26
|
"author": "",
|
|
17
27
|
"license": "ISC",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"@types/cors": "2.8.19",
|
|
21
|
-
"@types/node": "20.19.24",
|
|
22
|
-
"@types/ws": "^8.18.1",
|
|
23
|
-
"cors": "^2.8.5",
|
|
24
|
-
"express": "^4.19.2",
|
|
25
|
-
"ws": "^8.17.0"
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
26
30
|
},
|
|
27
|
-
"
|
|
28
|
-
"@types/
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@types/cors": "2.8.19",
|
|
33
|
+
"@types/express": "^4.17.21",
|
|
34
|
+
"@types/node": "20.19.24",
|
|
35
|
+
"@types/ws": "^8.18.1",
|
|
36
|
+
"cors": "^2.8.5",
|
|
37
|
+
"express": "^4.22.2",
|
|
38
|
+
"ws": "^8.21.3"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/jest": "^29.5.12",
|
|
29
42
|
"jest": "^29.7.0",
|
|
30
43
|
"supertest": "^7.0.0",
|
|
31
44
|
"typescript": "^5.9.3"
|
package/src/htmx/HtmxRenderer.js
CHANGED
|
@@ -8,7 +8,7 @@ class HtmxRenderer {
|
|
|
8
8
|
* @param {string} filePath - Path to the .htmx file.
|
|
9
9
|
* @returns {string} Rendered HTML string with normalized whitespace.
|
|
10
10
|
*/
|
|
11
|
-
static render(filePath, { rootDir = path.dirname(path.resolve(filePath)), timeoutMs = 1000 } = {}) {
|
|
11
|
+
static render(filePath, { rootDir = path.dirname(path.resolve(filePath)), timeoutMs = 1000 } = {}) {
|
|
12
12
|
if (!fs.existsSync(filePath)) {
|
|
13
13
|
throw new Error(`Template file not found: ${filePath}`);
|
|
14
14
|
}
|
|
@@ -32,17 +32,17 @@ class HtmxRenderer {
|
|
|
32
32
|
`;
|
|
33
33
|
|
|
34
34
|
// Create a custom require function that resolves paths relative to the template
|
|
35
|
-
const resolvedRoot = path.resolve(rootDir);
|
|
36
|
-
const customRequire = (modulePath) => {
|
|
37
|
-
if (typeof modulePath !== 'string' || !modulePath.startsWith('.')) {
|
|
38
|
-
throw new Error('Templates may only require relative modules');
|
|
39
|
-
}
|
|
40
|
-
const absolutePath = path.resolve(path.dirname(filePath), modulePath);
|
|
41
|
-
const relative = path.relative(resolvedRoot, absolutePath);
|
|
42
|
-
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
43
|
-
throw new Error('Template module is outside the allowed root');
|
|
44
|
-
}
|
|
45
|
-
return require(absolutePath);
|
|
35
|
+
const resolvedRoot = path.resolve(rootDir);
|
|
36
|
+
const customRequire = (modulePath) => {
|
|
37
|
+
if (typeof modulePath !== 'string' || !modulePath.startsWith('.')) {
|
|
38
|
+
throw new Error('Templates may only require relative modules');
|
|
39
|
+
}
|
|
40
|
+
const absolutePath = path.resolve(path.dirname(filePath), modulePath);
|
|
41
|
+
const relative = path.relative(resolvedRoot, absolutePath);
|
|
42
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
43
|
+
throw new Error('Template module is outside the allowed root');
|
|
44
|
+
}
|
|
45
|
+
return require(absolutePath);
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
// Execute the script in a sandbox
|
|
@@ -56,7 +56,7 @@ class HtmxRenderer {
|
|
|
56
56
|
vm.createContext(sandbox);
|
|
57
57
|
|
|
58
58
|
// Get the rendered output
|
|
59
|
-
let result = script.runInContext(sandbox, { timeout: timeoutMs });
|
|
59
|
+
let result = script.runInContext(sandbox, { timeout: timeoutMs });
|
|
60
60
|
|
|
61
61
|
// Normalize spaces but preserve those in content
|
|
62
62
|
result = result
|