redweb 0.7.7 → 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 -276
- 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 +308 -60
- package/index.js +24 -6
- package/package.json +25 -6
- package/src/htmx/HtmxRenderer.js +10 -2
- package/src/http/BaseHttpServer.js +84 -29
- package/src/http/HttpServer.js +18 -10
- package/src/http/HttpsServer.js +19 -11
- package/src/serverLifecycle.js +46 -0
- package/src/ws/AdmissionPolicy.js +145 -0
- package/src/ws/BaseHandler.js +40 -32
- package/src/ws/BaseSocketServer.js +182 -19
- package/src/ws/DefaultHandler.js +2 -3
- package/src/ws/DefaultRoute.js +4 -3
- 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 -12
- package/src/ws/SessionRegistry.js +135 -0
- package/src/ws/SocketRoute.js +503 -116
- package/src/ws/SocketServer.js +8 -11
- 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 -0
- package/src/ws/util.js +34 -5
package/index.d.ts
CHANGED
|
@@ -1,9 +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';
|
|
4
|
+
import { Server as NodeHttpServer } from 'http';
|
|
5
|
+
import { WebSocket, ServerOptions } from 'ws';
|
|
6
|
+
import { Buffer } from 'buffer';
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
7
8
|
|
|
8
9
|
/** ─────────────────── HTTP / CORE ─────────────────── */
|
|
9
10
|
|
|
@@ -12,42 +13,217 @@ declare module 'redweb' {
|
|
|
12
13
|
export interface RedWebOptions {
|
|
13
14
|
port?: number;
|
|
14
15
|
bind?: string;
|
|
15
|
-
publicPaths?: string[];
|
|
16
|
-
services?: Array<{ serviceName: string; method: string; function: Function }>;
|
|
17
|
-
listen?: boolean;
|
|
18
|
-
listenCallback?: () => void;
|
|
16
|
+
publicPaths?: string[];
|
|
17
|
+
services?: Array<{ serviceName: string; method: string; function: Function }>;
|
|
18
|
+
listen?: boolean;
|
|
19
|
+
listenCallback?: () => void;
|
|
19
20
|
encoding?: RedWebEncoding;
|
|
20
21
|
ssl?: { key: string; cert: string };
|
|
21
22
|
server?: Application;
|
|
22
|
-
corsOptions?: CorsOptions;
|
|
23
|
+
corsOptions?: CorsOptions | false;
|
|
23
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;
|
|
24
172
|
}
|
|
25
173
|
|
|
26
174
|
/** ─────────────────── SOCKET SERVER ─────────────────── */
|
|
27
175
|
|
|
28
176
|
export interface SocketServerOptions {
|
|
29
|
-
server?: NodeHttpServer;
|
|
30
|
-
port?: number;
|
|
31
|
-
|
|
32
|
-
|
|
177
|
+
server?: NodeHttpServer;
|
|
178
|
+
port?: number;
|
|
179
|
+
bind?: string;
|
|
180
|
+
listen?: boolean;
|
|
181
|
+
routes?: Array<new () => SocketRoute>;
|
|
33
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;
|
|
34
193
|
}
|
|
35
194
|
|
|
36
195
|
/** ─────────────────── ROUTES & HANDLERS ─────────────────── */
|
|
37
196
|
|
|
38
197
|
export interface SocketRouteConfig {
|
|
39
198
|
path: string;
|
|
40
|
-
handlers: Array<new () => BaseHandler>;
|
|
41
|
-
services?: Array<new () => SocketService>;
|
|
42
|
-
allowDuplicateConnections?: boolean;
|
|
43
|
-
websocketOptions?: ServerOptions
|
|
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;
|
|
44
219
|
}
|
|
45
220
|
|
|
46
221
|
/** Socket‑side autonomous service (game loops, timers, etc.) */
|
|
47
222
|
export abstract class SocketService {
|
|
48
223
|
name: string;
|
|
49
|
-
tickRateMs
|
|
50
|
-
|
|
224
|
+
tickRateMs: number | null;
|
|
225
|
+
route: SocketRoute;
|
|
226
|
+
protected _tickHandle: NodeJS.Timeout | null;
|
|
51
227
|
|
|
52
228
|
constructor(name: string, tickRateMs?: number);
|
|
53
229
|
|
|
@@ -55,62 +231,116 @@ declare module 'redweb' {
|
|
|
55
231
|
onInit(route: SocketRoute): void;
|
|
56
232
|
|
|
57
233
|
/** Optional recurring tick (respecting tickRateMs) */
|
|
58
|
-
onTick?():
|
|
234
|
+
onTick?(...args: any[]): unknown;
|
|
59
235
|
|
|
60
236
|
/** Called on process shutdown / route removal */
|
|
61
237
|
onShutdown(): void;
|
|
62
238
|
}
|
|
63
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
|
+
|
|
64
253
|
/** Message handler, triggered by client messages */
|
|
65
254
|
export class BaseHandler {
|
|
66
255
|
name: string;
|
|
67
256
|
constructor(name: string);
|
|
68
257
|
|
|
69
258
|
handleMessage(
|
|
70
|
-
socket:
|
|
71
|
-
sendJson: (message: object) => void;
|
|
72
|
-
broadcast: (message: object) => void;
|
|
73
|
-
},
|
|
259
|
+
socket: RedWebSocket,
|
|
74
260
|
message: any
|
|
75
|
-
):
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
}
|
|
83
270
|
|
|
84
271
|
export class SocketRoute {
|
|
85
272
|
path: string;
|
|
86
|
-
handlers: BaseHandler[];
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
+
}
|
|
97
297
|
|
|
98
298
|
/** ─────────────────── SERVER BASE ─────────────────── */
|
|
99
299
|
|
|
100
300
|
export class BaseSocketServer {
|
|
101
|
-
|
|
102
|
-
server: NodeHttpServer;
|
|
301
|
+
server: NodeHttpServer;
|
|
103
302
|
routes: SocketRoute[];
|
|
303
|
+
ownsServer: boolean;
|
|
104
304
|
|
|
105
|
-
constructor(server: NodeHttpServer, options?: SocketServerOptions);
|
|
305
|
+
constructor(server: NodeHttpServer, options?: SocketServerOptions);
|
|
106
306
|
|
|
107
|
-
addRoute(route: new () => SocketRoute):
|
|
307
|
+
addRoute(route: new () => SocketRoute): SocketRoute;
|
|
308
|
+
isReady(): boolean;
|
|
309
|
+
beginDrain(): boolean;
|
|
310
|
+
shutdown(): Promise<void>;
|
|
108
311
|
}
|
|
109
312
|
|
|
110
313
|
/** ─────────────────── REGISTRY & UTIL TYPES ─────────────────── */
|
|
111
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;
|
|
328
|
+
}
|
|
329
|
+
|
|
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
|
+
}
|
|
341
|
+
|
|
112
342
|
export interface SocketWrapper {
|
|
113
|
-
socket:
|
|
343
|
+
socket: RedWebSocket;
|
|
114
344
|
id: string;
|
|
115
345
|
send: (type: string, payload: Record<string, any>) => void;
|
|
116
346
|
getSanitized?(): Record<string, any>;
|
|
@@ -155,19 +385,22 @@ declare module 'redweb' {
|
|
|
155
385
|
constructor(options?: SocketServerOptions);
|
|
156
386
|
}
|
|
157
387
|
|
|
158
|
-
export class BaseHttpServer {
|
|
159
|
-
app: Application;
|
|
160
|
-
server?: NodeHttpServer;
|
|
161
|
-
constructor(options?: RedWebOptions);
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
}
|
|
171
404
|
|
|
172
405
|
/** ─────────────────── CONSTANTS ─────────────────── */
|
|
173
406
|
|
|
@@ -175,7 +408,11 @@ declare module 'redweb' {
|
|
|
175
408
|
GET: 'get';
|
|
176
409
|
POST: 'post';
|
|
177
410
|
PUT: 'put';
|
|
411
|
+
PATCH: 'patch';
|
|
178
412
|
DELETE: 'delete';
|
|
413
|
+
OPTIONS: 'options';
|
|
414
|
+
HEAD: 'head';
|
|
415
|
+
ALL: 'all';
|
|
179
416
|
};
|
|
180
417
|
|
|
181
418
|
export const ENCODINGS: {
|
|
@@ -185,4 +422,15 @@ declare module 'redweb' {
|
|
|
185
422
|
|
|
186
423
|
export const HTTP_OPTIONS: RedWebOptions;
|
|
187
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
|
+
}>;
|
|
188
436
|
}
|
package/index.js
CHANGED
|
@@ -1,20 +1,38 @@
|
|
|
1
|
-
const { BaseHttpServer, METHODS } = require('./src/http');
|
|
1
|
+
const { BaseHttpServer, METHODS } = require('./src/http');
|
|
2
|
+
const { ENCODINGS, HTTP_OPTIONS } = require('./src/http/BaseHttpServer');
|
|
2
3
|
const { sendJson } = require('./src/ws/util');
|
|
3
|
-
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');
|
|
4
16
|
const { BaseHandler } = require('./src/ws/BaseHandler');
|
|
5
17
|
const HttpServer = require('./src/http/HttpServer');
|
|
6
18
|
const HttpsServer = require('./src/http/HttpsServer');
|
|
7
19
|
module.exports = {
|
|
8
|
-
HttpServer,
|
|
9
|
-
HttpsServer,
|
|
10
|
-
BaseHttpServer,
|
|
11
|
-
SocketServer,
|
|
20
|
+
HttpServer,
|
|
21
|
+
HttpsServer,
|
|
22
|
+
BaseHttpServer,
|
|
23
|
+
SocketServer,
|
|
12
24
|
SecureSocketServer,
|
|
13
25
|
BaseHandler,
|
|
14
26
|
SocketRoute,
|
|
15
27
|
SocketService,
|
|
28
|
+
FixedStepService,
|
|
16
29
|
SocketRegistry,
|
|
30
|
+
RoomRegistry,
|
|
31
|
+
SessionRegistry,
|
|
32
|
+
ERROR_CODES,
|
|
17
33
|
sendJson,
|
|
18
34
|
SOCKET_OPTIONS,
|
|
35
|
+
HTTP_OPTIONS,
|
|
36
|
+
ENCODINGS,
|
|
19
37
|
METHODS
|
|
20
38
|
};
|
package/package.json
CHANGED
|
@@ -1,27 +1,46 @@
|
|
|
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
|
+
"types": "index.d.ts",
|
|
6
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",
|
|
7
15
|
"test": "npx jest"
|
|
8
16
|
},
|
|
9
17
|
"files": [
|
|
10
18
|
"src/*",
|
|
11
|
-
"
|
|
19
|
+
"client.js",
|
|
20
|
+
"client.d.ts",
|
|
21
|
+
"CHANGELOG.md",
|
|
22
|
+
"index.d.ts",
|
|
23
|
+
"docs"
|
|
12
24
|
],
|
|
13
25
|
"keywords": [],
|
|
14
26
|
"author": "",
|
|
15
27
|
"license": "ISC",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
16
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",
|
|
17
36
|
"cors": "^2.8.5",
|
|
18
|
-
"express": "^4.
|
|
19
|
-
"ws": "^8.
|
|
37
|
+
"express": "^4.22.2",
|
|
38
|
+
"ws": "^8.21.3"
|
|
20
39
|
},
|
|
21
40
|
"devDependencies": {
|
|
22
|
-
"@types/express": "^4.17.21",
|
|
23
41
|
"@types/jest": "^29.5.12",
|
|
24
42
|
"jest": "^29.7.0",
|
|
25
|
-
"supertest": "^7.0.0"
|
|
43
|
+
"supertest": "^7.0.0",
|
|
44
|
+
"typescript": "^5.9.3"
|
|
26
45
|
}
|
|
27
46
|
}
|
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) {
|
|
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,8 +32,16 @@ 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);
|
|
35
36
|
const customRequire = (modulePath) => {
|
|
37
|
+
if (typeof modulePath !== 'string' || !modulePath.startsWith('.')) {
|
|
38
|
+
throw new Error('Templates may only require relative modules');
|
|
39
|
+
}
|
|
36
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
|
+
}
|
|
37
45
|
return require(absolutePath);
|
|
38
46
|
};
|
|
39
47
|
|
|
@@ -48,7 +56,7 @@ class HtmxRenderer {
|
|
|
48
56
|
vm.createContext(sandbox);
|
|
49
57
|
|
|
50
58
|
// Get the rendered output
|
|
51
|
-
let result = script.runInContext(sandbox);
|
|
59
|
+
let result = script.runInContext(sandbox, { timeout: timeoutMs });
|
|
52
60
|
|
|
53
61
|
// Normalize spaces but preserve those in content
|
|
54
62
|
result = result
|