redweb 0.8.0 → 0.10.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 (72) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +573 -307
  3. package/client.d.ts +42 -0
  4. package/client.js +55 -0
  5. package/docs/LIVE_HTML.md +313 -0
  6. package/docs/MULTIPLAYER_OPERATIONS.md +50 -0
  7. package/docs/PRODUCTION_READINESS.md +68 -0
  8. package/docs/VERIFICATION_EVIDENCE.md +20 -0
  9. package/examples/live-html/cards.css +36 -0
  10. package/examples/live-html/cards.html +11 -0
  11. package/examples/live-html/cards.js +91 -0
  12. package/examples/live-html/cards.ts +35 -0
  13. package/examples/live-html/chatroom.css +156 -0
  14. package/examples/live-html/chatroom.js +268 -0
  15. package/examples/live-html/chatroom.ts +217 -0
  16. package/examples/live-html/components.css +7 -0
  17. package/examples/live-html/components.js +113 -0
  18. package/examples/live-html/components.ts +41 -0
  19. package/examples/live-html/counter.css +24 -0
  20. package/examples/live-html/counter.html +10 -0
  21. package/examples/live-html/counter.js +73 -0
  22. package/examples/live-html/counter.ts +21 -0
  23. package/examples/live-html/tsconfig.json +16 -0
  24. package/index.d.ts +538 -114
  25. package/index.js +44 -12
  26. package/package.json +39 -15
  27. package/src/htmx/Html.js +133 -0
  28. package/src/htmx/HtmlRenderer.js +88 -0
  29. package/src/htmx/HtmlSyntax.js +168 -0
  30. package/src/htmx/LiveHtmlServer.js +91 -0
  31. package/src/htmx/LivePage.js +232 -0
  32. package/src/htmx/PageAssetLoader.js +34 -0
  33. package/src/htmx/PageManager.js +435 -0
  34. package/src/htmx/StaticExporter.js +78 -0
  35. package/src/htmx/StaticSite.js +182 -0
  36. package/src/htmx/TemplateRenderer.js +231 -0
  37. package/src/htmx/browserRuntime.js +97 -0
  38. package/src/htmx/index.js +10 -0
  39. package/src/htmx/metadata.js +349 -0
  40. package/src/htmx/sourceRoot.js +28 -0
  41. package/src/htmx/start.js +17 -0
  42. package/src/htmx/synchronous.js +9 -0
  43. package/src/http/BaseHttpServer.js +82 -117
  44. package/src/http/HttpServer.js +18 -18
  45. package/src/http/HttpsServer.js +20 -20
  46. package/src/serverLifecycle.js +46 -46
  47. package/src/ws/AdmissionPolicy.js +145 -0
  48. package/src/ws/BaseHandler.js +40 -40
  49. package/src/ws/BaseSocketServer.js +199 -100
  50. package/src/ws/DefaultHandler.js +5 -5
  51. package/src/ws/DefaultRoute.js +8 -8
  52. package/src/ws/DistributionBridge.js +271 -0
  53. package/src/ws/FixedStepService.js +74 -0
  54. package/src/ws/HeartbeatMonitor.js +75 -0
  55. package/src/ws/Metrics.js +34 -0
  56. package/src/ws/ProtocolPolicy.js +130 -0
  57. package/src/ws/RoomRegistry.js +117 -0
  58. package/src/ws/RouteRuntime.js +146 -0
  59. package/src/ws/SecureSocketServer.js +9 -9
  60. package/src/ws/SessionRegistry.js +135 -0
  61. package/src/ws/SocketRoute.js +523 -254
  62. package/src/ws/SocketServer.js +8 -8
  63. package/src/ws/TaskQueue.js +64 -0
  64. package/src/ws/TokenBucket.js +31 -0
  65. package/src/ws/TransportPolicy.js +68 -0
  66. package/src/ws/index.js +7 -2
  67. package/src/ws/protocol-schema.json +13 -0
  68. package/src/ws/protocol-validation.js +21 -0
  69. package/src/ws/shutdown.js +33 -33
  70. package/src/ws/util.js +38 -30
  71. package/src/htmx/HtmxRenderer.js +0 -73
  72. package/src/htmx/RedWebHtmxComponent.js +0 -11
package/index.d.ts CHANGED
@@ -1,10 +1,11 @@
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 { Server as NodeHttpsServer } from 'https';
6
+ import { WebSocket, ServerOptions } from 'ws';
7
+ import { Buffer } from 'buffer';
8
+ import { EventEmitter } from 'events';
8
9
 
9
10
  /** ─────────────────── HTTP / CORE ─────────────────── */
10
11
 
@@ -13,70 +14,216 @@ declare module 'redweb' {
13
14
  export interface RedWebOptions {
14
15
  port?: number;
15
16
  bind?: string;
16
- publicPaths?: string[];
17
- services?: Array<{ serviceName: string; method: string; function: Function }>;
18
- listen?: boolean;
19
- listenCallback?: () => void;
17
+ publicPaths?: string[];
18
+ services?: Array<{ serviceName: string; method: string; function: Function }>;
19
+ listen?: boolean;
20
+ listenCallback?: () => void;
20
21
  encoding?: RedWebEncoding;
21
22
  ssl?: { key: string; cert: string };
22
23
  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
- };
24
+ corsOptions?: CorsOptions | false;
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;
136
304
 
137
- constructor(server: NodeHttpServer, options?: SocketServerOptions);
305
+ constructor(server: NodeHttpServer, options?: SocketServerOptions);
138
306
 
139
- addRoute(route: new () => SocketRoute): SocketRoute;
140
- shutdown(): Promise<void>;
307
+ addRoute(route: new () => SocketRoute): SocketRoute;
308
+ isReady(): boolean;
309
+ beginDrain(): boolean;
310
+ shutdown(): Promise<void>;
141
311
  }
142
312
 
143
- /** ─────────────────── REGISTRY & UTIL TYPES ─────────────────── */
144
-
145
- export function sendJson(socket: WebSocket, data: unknown): boolean;
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;
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
+ }
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,252 @@ 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
+ }
404
+
405
+ /** ─────────────────── LIVE HTML ─────────────────── */
406
+
407
+ const htmlFragmentBrand: unique symbol;
408
+ const htmlAttributeBrand: unique symbol;
409
+ const htmlUrlBrand: unique symbol;
410
+
411
+ export interface HtmlFragment {
412
+ readonly [htmlFragmentBrand]: true;
413
+ toString(): string;
414
+ }
415
+
416
+ export interface HtmlAttribute {
417
+ readonly [htmlAttributeBrand]: true;
418
+ }
419
+
420
+ export interface HtmlUrl {
421
+ readonly [htmlUrlBrand]: true;
422
+ }
423
+
424
+ export interface LivePageRequest {
425
+ readonly path: string;
426
+ readonly url: string;
427
+ readonly method: string;
428
+ readonly headers: import('http').IncomingHttpHeaders;
429
+ readonly params: Readonly<Record<string, string>>;
430
+ readonly query: Readonly<Record<string, unknown>>;
431
+ readonly body: unknown;
432
+ get(name: string): string | undefined;
433
+ }
434
+
435
+ export interface LivePageRequestContext {
436
+ request: LivePageRequest;
437
+ params: Readonly<Record<string, string>>;
438
+ query: Readonly<Record<string, unknown>>;
439
+ body: unknown;
440
+ principal?: string | number | bigint | boolean;
441
+ signal: AbortSignal;
442
+ }
443
+
444
+ export interface LivePageConnectionContext {
445
+ socket: RedWebSocket;
446
+ signal?: AbortSignal;
447
+ principal?: string | number | bigint | boolean;
448
+ }
449
+
450
+ export abstract class LivePage {
451
+ protected readonly _connections: Set<RedWebSocket>;
452
+ loading?(context: LivePageRequestContext): void | Promise<void>;
453
+ render?(context: LivePageRequestContext): string | HtmlFragment | Promise<string | HtmlFragment>;
454
+ connected?(context: LivePageConnectionContext): void | Promise<void>;
455
+ disconnected?(context: { socket: RedWebSocket }): void | Promise<void>;
456
+ disposed?(): void | Promise<void>;
457
+ dispose(): Promise<boolean>;
458
+ }
459
+
460
+ export interface PageOptions {
461
+ template?: string;
462
+ css?: string | readonly string[];
463
+ scope?: 'connection' | 'shared';
464
+ shared?: boolean;
465
+ live?: boolean;
466
+ head?: PageHead;
467
+ cache?: PageCache;
468
+ layout?: PageLayout;
469
+ }
470
+
471
+ export type PageLayout = (content: HtmlFragment, context: LivePageRequestContext) => HtmlFragment;
472
+
473
+ export interface PageHead {
474
+ title?: string;
475
+ description?: string;
476
+ canonical?: string;
477
+ image?: string;
478
+ robots?: string;
479
+ }
480
+
481
+ export interface PageCache {
482
+ maxAge?: number;
483
+ staleWhileRevalidate?: number;
484
+ immutable?: boolean;
485
+ }
486
+
487
+ export interface StateOptions {
488
+ writable?: boolean;
489
+ }
490
+
491
+ export interface LiveStateDecorator {
492
+ (target: object, propertyKey: string): void;
493
+ <This, Value>(value: undefined, context: ClassFieldDecoratorContext<This, Value>):
494
+ (this: This, initialValue: Value) => Value;
495
+ }
496
+
497
+ export interface LiveActionDecorator {
498
+ (target: object, propertyKey: string, descriptor: PropertyDescriptor): void | PropertyDescriptor;
499
+ <This, Value extends (this: This, ...args: any[]) => any>(
500
+ value: Value,
501
+ context: ClassMethodDecoratorContext<This, Value>
502
+ ): Value;
503
+ }
504
+
505
+ export interface LiveViewDecorator {
506
+ (target: object, propertyKey: string, descriptor: PropertyDescriptor): void | PropertyDescriptor;
507
+ <This, Value extends (this: This, item: any, index: number) => HtmlFragment>(
508
+ value: Value,
509
+ context: ClassMethodDecoratorContext<This, Value>
510
+ ): Value;
511
+ }
512
+
513
+ export function page(path: string, options?: PageOptions): ClassDecorator;
514
+ export function component(): ClassDecorator;
515
+ export function component<Props = void>(render: (properties: Props) => HtmlFragment):
516
+ (properties: Props) => HtmlFragment;
517
+ export function state(options?: StateOptions): LiveStateDecorator;
518
+ export function action(): LiveActionDecorator;
519
+ export function view(stateName: string): LiveViewDecorator;
520
+ export function html(strings: TemplateStringsArray, ...values: unknown[]): HtmlFragment;
521
+ export function attribute(value: string | number | bigint | boolean): HtmlAttribute;
522
+ export function url(value: string): HtmlUrl;
523
+ export function each<Item>(items: readonly Item[], render: (item: Item, index: number) => HtmlFragment): HtmlFragment;
524
+ export function codeBlock(code: unknown, options?: {
525
+ language?: string;
526
+ label?: string;
527
+ highlight?: (source: string, language: string) => HtmlFragment;
528
+ }): HtmlFragment;
529
+
530
+ export type LivePageClass = new () => object;
531
+
532
+ export interface LiveHtmlServerOptions extends Omit<RedWebOptions, 'enableHtmxRendering'> {
533
+ pages: readonly LivePageClass[];
534
+ templateRoot?: string;
535
+ livePaths?: {
536
+ socket?: string;
537
+ client?: string;
538
+ runtime?: string;
539
+ css?: string;
540
+ };
541
+ sessionTtlMs?: number;
542
+ maxSessions?: number;
543
+ maxConcurrentRenders?: number;
544
+ shutdownTimeoutMs?: number;
545
+ heartbeat?: HeartbeatOptions;
546
+ authenticate?(request: import('http').IncomingMessage | import('express').Request):
547
+ string | number | bigint | boolean | false | null | undefined |
548
+ Promise<string | number | bigint | boolean | false | null | undefined>;
549
+ origins?: string[] | ((origin: string | undefined, request: import('http').IncomingMessage) => boolean | Promise<boolean>);
550
+ }
551
+
552
+ export class LiveHtmlServer {
553
+ app: Application;
554
+ server: NodeHttpServer | NodeHttpsServer;
555
+ http: HttpServer | HttpsServer;
556
+ sockets: SocketServer | null;
557
+ constructor(options: LiveHtmlServerOptions);
558
+ shutdown(): Promise<void>;
559
+ }
560
+
561
+ export function start(
562
+ pageOrPages: LivePageClass | readonly LivePageClass[],
563
+ options?: Omit<LiveHtmlServerOptions, 'pages'>
564
+ ): LiveHtmlServer;
565
+
566
+ export interface StaticExportOptions {
567
+ outDir: string;
568
+ templateRoot?: string;
569
+ logger?: RedWebLogger | null;
570
+ }
571
+
572
+ export interface StaticExportResult {
573
+ readonly pages: readonly string[];
574
+ readonly assets: readonly string[];
575
+ }
576
+
577
+ export function exportStatic(
578
+ pageOrPages: LivePageClass | readonly LivePageClass[],
579
+ options: StaticExportOptions
580
+ ): Promise<StaticExportResult>;
581
+
582
+ export interface SiteOptions {
583
+ origin?: string;
584
+ css?: string | readonly string[];
585
+ head?: PageHead;
586
+ cache?: PageCache;
587
+ layout?: PageLayout;
588
+ }
589
+
590
+ export interface SitePageOptions extends Omit<PageOptions, 'live'> {
591
+ live?: false;
592
+ }
593
+
594
+ export interface SiteExportOptions extends StaticExportOptions {
595
+ publicDir?: string;
596
+ }
597
+
598
+ export interface StaticSite {
599
+ page(path: string, options?: SitePageOptions): ClassDecorator;
600
+ export(
601
+ pageOrPages: LivePageClass | readonly LivePageClass[],
602
+ options: SiteExportOptions
603
+ ): Promise<StaticExportResult>;
604
+ }
605
+
606
+ export function defineSite(options?: SiteOptions): StaticSite;
607
+
608
+ export class HtmlRenderer {
609
+ static template(filePath: string, rootDir: string): string;
610
+ static stylesheet(filePath: string, rootDir: string): string;
611
+ static render(source: string, page: object, options?: { live?: boolean }): string;
612
+ static collection(page: object, name: string, value: unknown): string;
613
+ static statePayload(name: string, value: unknown, page?: object): { name: string; value: string; html: boolean };
614
+ static head(metadata?: PageHead): string;
615
+ static document(
616
+ markup: string,
617
+ config?: (Record<string, unknown> & { runtimePath: string }) | null,
618
+ stylesheets?: string[],
619
+ metadata?: PageHead
620
+ ): string;
621
+ }
209
622
 
210
623
  /** ─────────────────── CONSTANTS ─────────────────── */
211
624
 
212
625
  export const METHODS: {
213
626
  GET: 'get';
214
627
  POST: 'post';
215
- PUT: 'put';
216
- PATCH: 'patch';
217
- DELETE: 'delete';
218
- OPTIONS: 'options';
219
- HEAD: 'head';
220
- ALL: 'all';
628
+ PUT: 'put';
629
+ PATCH: 'patch';
630
+ DELETE: 'delete';
631
+ OPTIONS: 'options';
632
+ HEAD: 'head';
633
+ ALL: 'all';
221
634
  };
222
635
 
223
636
  export const ENCODINGS: {
@@ -227,4 +640,15 @@ declare module 'redweb' {
227
640
 
228
641
  export const HTTP_OPTIONS: RedWebOptions;
229
642
  export const SOCKET_OPTIONS: SocketServerOptions;
643
+
644
+ export const ERROR_CODES: Readonly<{
645
+ INVALID_MESSAGE: 'INVALID_MESSAGE';
646
+ UNKNOWN_HANDLER: 'UNKNOWN_HANDLER';
647
+ HANDLER_FAILED: 'HANDLER_FAILED';
648
+ BINARY_UNSUPPORTED: 'BINARY_UNSUPPORTED';
649
+ RATE_LIMITED: 'RATE_LIMITED';
650
+ QUEUE_FULL: 'QUEUE_FULL';
651
+ CAPACITY_REACHED: 'CAPACITY_REACHED';
652
+ INITIALIZATION_FAILED: 'INITIALIZATION_FAILED';
653
+ }>;
230
654
  }