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.
Files changed (39) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +458 -307
  3. package/client.d.ts +42 -0
  4. package/client.js +55 -0
  5. package/docs/MULTIPLAYER_OPERATIONS.md +50 -0
  6. package/docs/PRODUCTION_READINESS.md +68 -0
  7. package/docs/VERIFICATION_EVIDENCE.md +20 -0
  8. package/index.d.ts +320 -114
  9. package/index.js +27 -12
  10. package/package.json +28 -15
  11. package/src/htmx/HtmxRenderer.js +13 -13
  12. package/src/http/BaseHttpServer.js +112 -112
  13. package/src/http/HttpServer.js +18 -18
  14. package/src/http/HttpsServer.js +20 -20
  15. package/src/serverLifecycle.js +46 -46
  16. package/src/ws/AdmissionPolicy.js +145 -0
  17. package/src/ws/BaseHandler.js +40 -40
  18. package/src/ws/BaseSocketServer.js +195 -100
  19. package/src/ws/DefaultHandler.js +5 -5
  20. package/src/ws/DefaultRoute.js +8 -8
  21. package/src/ws/DistributionBridge.js +271 -0
  22. package/src/ws/FixedStepService.js +74 -0
  23. package/src/ws/HeartbeatMonitor.js +75 -0
  24. package/src/ws/Metrics.js +34 -0
  25. package/src/ws/ProtocolPolicy.js +130 -0
  26. package/src/ws/RoomRegistry.js +117 -0
  27. package/src/ws/RouteRuntime.js +146 -0
  28. package/src/ws/SecureSocketServer.js +9 -9
  29. package/src/ws/SessionRegistry.js +135 -0
  30. package/src/ws/SocketRoute.js +523 -254
  31. package/src/ws/SocketServer.js +8 -8
  32. package/src/ws/TaskQueue.js +64 -0
  33. package/src/ws/TokenBucket.js +31 -0
  34. package/src/ws/TransportPolicy.js +68 -0
  35. package/src/ws/index.js +7 -2
  36. package/src/ws/protocol-schema.json +13 -0
  37. package/src/ws/protocol-validation.js +21 -0
  38. package/src/ws/shutdown.js +33 -33
  39. 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?(): void;
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
- allowDuplicateConnections?: boolean;
117
- websocketOptions?: SocketRouteConfig['websocketOptions'];
118
-
119
- constructor(config: SocketRouteConfig);
120
-
121
- addHandler(handler: new () => BaseHandler): boolean;
122
- resolveRemoteAddress(request: import('http').IncomingMessage): string;
123
- connectionOpenCallback(socket: RedWebSocket, request?: import('http').IncomingMessage): unknown;
124
- connectionCloseCallback?(socket: RedWebSocket): unknown;
125
- handleMessage(sock: RedWebSocket, data: any): Promise<boolean>;
126
- handleBinaryMessage(socket: RedWebSocket, buffer: Buffer): Promise<boolean>;
127
- shutdown(): Promise<void>;
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
- constructor(server: NodeHttpServer, options?: SocketServerOptions);
307
+ addRoute(route: new () => SocketRoute): SocketRoute;
308
+ isReady(): boolean;
309
+ beginDrain(): boolean;
310
+ shutdown(): Promise<void>;
311
+ }
138
312
 
139
- addRoute(route: new () => SocketRoute): SocketRoute;
140
- shutdown(): Promise<void>;
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
- /** ─────────────────── REGISTRY & UTIL TYPES ─────────────────── */
144
-
145
- export function sendJson(socket: WebSocket, data: unknown): boolean;
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 { SocketServer, SecureSocketServer, SOCKET_OPTIONS, SocketRoute, SocketService, SocketRegistry } = require('./src/ws');
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.8.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
- "test": "npx jest"
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
- "index.d.ts"
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
- "dependencies": {
19
- "@types/express": "^4.17.21",
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
- "devDependencies": {
28
- "@types/jest": "^29.5.12",
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"
@@ -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