redweb 0.11.0 → 0.13.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 (167) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/README.md +176 -508
  3. package/bin/redweb.js +11 -0
  4. package/client.d.ts +7 -2
  5. package/config/tsconfig.json +14 -0
  6. package/contract.d.ts +45 -0
  7. package/contract.js +5 -0
  8. package/docs/ACTION_INPUT_VERIFICATION.md +96 -0
  9. package/docs/ADMISSION_TIMEOUT_VERIFICATION.md +69 -0
  10. package/docs/AGENT_ACCESS.md +35 -0
  11. package/docs/AGENT_EVALUATION.md +58 -0
  12. package/docs/AGENT_READY_ACCEPTANCE.md +763 -0
  13. package/docs/APPLICATION_RECORDER_VERIFICATION.md +50 -0
  14. package/docs/BENCHMARK_VERIFICATION.md +307 -0
  15. package/docs/BROWSER_OWNER_VERIFICATION.md +191 -0
  16. package/docs/CLI.md +116 -0
  17. package/docs/CLIENT_DEVELOPMENT.md +152 -0
  18. package/docs/CLIENT_POLISH_VERIFICATION.md +282 -0
  19. package/docs/COVERAGE_COUNTER_VALIDATION.md +109 -0
  20. package/docs/COVERAGE_SCOPE_AUDIT.md +1183 -0
  21. package/docs/DEVELOPMENT.md +79 -0
  22. package/docs/DIAGNOSTIC_COMPATIBILITY.md +76 -0
  23. package/docs/DOCUMENTATION.md +37 -0
  24. package/docs/FEEDBACK_COMMAND_VERIFICATION.md +228 -0
  25. package/docs/GETTING_STARTED.md +58 -0
  26. package/docs/JSX_PERFORMANCE_VERIFICATION.md +59 -0
  27. package/docs/LIVE_HTML.md +169 -8
  28. package/docs/LIVE_HTML_LOAD_VERIFICATION.md +98 -0
  29. package/docs/MIGRATION.md +28 -0
  30. package/docs/MULTIPLAYER_OPERATIONS.md +26 -4
  31. package/docs/ORIGINAL_RECOVERY_VERIFICATION.md +100 -0
  32. package/docs/PACKAGED_EXAMPLE_VERIFICATION.md +126 -0
  33. package/docs/POLISH_RELEASE_CHECKPOINT.md +91 -0
  34. package/docs/PROCESS_CLEANUP_OBSERVATION.md +61 -0
  35. package/docs/PROCESS_REAPING_VERIFICATION.md +30 -0
  36. package/docs/PRODUCTION_READINESS.md +11 -2
  37. package/docs/RECOVERY_CLIENT_HEAP.md +201 -0
  38. package/docs/RECOVERY_CODE_ATTRIBUTION.md +174 -0
  39. package/docs/RECOVERY_CODE_CENSUS.md +158 -0
  40. package/docs/RECOVERY_COMPARISON.md +103 -0
  41. package/docs/RECOVERY_DEOPTIMIZATION.md +169 -0
  42. package/docs/RECOVERY_FOLLOWUP_SPIKE.md +147 -0
  43. package/docs/RECOVERY_INVESTIGATION.md +229 -0
  44. package/docs/RECOVERY_RUNTIME_CONTROLS.md +181 -0
  45. package/docs/RELEASE_TRUST.md +58 -0
  46. package/docs/ROOM_AUTHORIZATION.md +49 -0
  47. package/docs/RUNTIME_DIAGNOSTICS.md +78 -0
  48. package/docs/SERVER_RECOVERY_CANDIDATE.md +185 -0
  49. package/docs/SOAK_ROTATION_OBSERVATION.md +160 -0
  50. package/docs/SOAK_VERIFICATION.md +154 -0
  51. package/docs/SOCKET_CONTRACTS.md +39 -0
  52. package/docs/SPLIT_RECOVERY_COVERAGE.md +83 -0
  53. package/docs/SPLIT_RECOVERY_ERROR_HANDLING.md +67 -0
  54. package/docs/STARTER_COORDINATOR_VERIFICATION.md +112 -0
  55. package/docs/STARTER_LIFECYCLE_VERIFICATION.md +75 -0
  56. package/docs/STARTER_REPORT_RETENTION.md +73 -0
  57. package/docs/VERIFICATION_EVIDENCE.md +2 -0
  58. package/docs/generated.json +2154 -0
  59. package/docs/guides/chatroom.md +27 -0
  60. package/docs/guides/http-websocket.md +28 -0
  61. package/docs/guides/jsx-without-react.md +26 -0
  62. package/docs/guides/realtime-dashboard.md +29 -0
  63. package/docs/guides/typed-websockets.md +26 -0
  64. package/docs/reference.json +1207 -0
  65. package/docs/snippets/room-access.tsx +51 -0
  66. package/docs/topics.json +21 -0
  67. package/examples/live-html/chatroom.js +207 -268
  68. package/examples/live-html/chatroom.tsx +167 -0
  69. package/examples/live-html/jsx-page.js +1 -1
  70. package/examples/live-html/jsx-page.tsx +1 -1
  71. package/examples/live-html/tsconfig.json +3 -8
  72. package/index.d.ts +170 -45
  73. package/index.js +2 -0
  74. package/jsx-dev-runtime.js +2 -2
  75. package/jsx-runtime.d.ts +7 -2
  76. package/package.json +94 -7
  77. package/recipes/add/artifact.test.cjs +57 -0
  78. package/recipes/add/live.tsx +18 -0
  79. package/recipes/add/socket-route.ts +24 -0
  80. package/recipes/chat/README.md +22 -0
  81. package/recipes/chat/app.test.cjs +105 -0
  82. package/recipes/chat/app.tsx +9 -0
  83. package/recipes/dashboard/README.md +43 -0
  84. package/recipes/dashboard/admin.ts +21 -0
  85. package/recipes/dashboard/app.css +16 -0
  86. package/recipes/dashboard/app.test.cjs +450 -0
  87. package/recipes/dashboard/app.tsx +86 -0
  88. package/recipes/dashboard/auth.ts +80 -0
  89. package/recipes/dashboard/cards.tsx +102 -0
  90. package/recipes/dashboard/rate-window.test.cjs +17 -0
  91. package/recipes/dashboard/store.ts +120 -0
  92. package/recipes/http-ws/README.md +11 -0
  93. package/recipes/http-ws/app.test.cjs +92 -0
  94. package/recipes/http-ws/app.tsx +36 -0
  95. package/recipes/realtime/README.md +8 -0
  96. package/recipes/realtime/app.test.cjs +15 -0
  97. package/recipes/realtime/app.tsx +28 -0
  98. package/recipes/shared/README.md +40 -0
  99. package/recipes/shared/app.css +8 -0
  100. package/recipes/shared/copy-assets.cjs +8 -0
  101. package/recipes/shared/network.cjs +59 -0
  102. package/recipes/shared/run-app.test.cjs +158 -0
  103. package/recipes/shared/run-app.ts +50 -0
  104. package/recipes/site/README.md +4 -0
  105. package/recipes/site/app.test.cjs +19 -0
  106. package/recipes/site/app.tsx +25 -0
  107. package/recipes/socket/README.md +39 -0
  108. package/recipes/socket/app.test.cjs +85 -0
  109. package/recipes/socket/app.tsx +30 -0
  110. package/recipes/socket/contract.ts +12 -0
  111. package/recipes/socket/handlers.ts +40 -0
  112. package/src/OwnedServerLifecycle.js +66 -0
  113. package/src/access/AccessPolicy.js +37 -0
  114. package/src/access/AuthenticationFailure.js +13 -0
  115. package/src/access/RequestFailure.js +33 -0
  116. package/src/access/failure-codes.json +25 -0
  117. package/src/async/BoundedOperation.js +62 -0
  118. package/src/cli/ActionReferences.js +193 -0
  119. package/src/cli/AdditionLayout.js +140 -0
  120. package/src/cli/FilePlan.js +94 -0
  121. package/src/cli/ProjectAddition.js +60 -0
  122. package/src/cli/ProjectConfig.js +26 -0
  123. package/src/cli/ProjectDoctor.js +112 -0
  124. package/src/cli/ProjectInitializer.js +20 -0
  125. package/src/cli/SourceInspector.js +207 -0
  126. package/src/cli/StaticSource.js +192 -0
  127. package/src/cli/arguments.js +62 -0
  128. package/src/cli/formatCommand.js +10 -0
  129. package/src/cli/run.js +57 -0
  130. package/src/cli/templates.js +86 -0
  131. package/src/context/RequestSnapshot.js +41 -0
  132. package/src/dataProperty.js +11 -0
  133. package/src/development/DevelopmentPageManager.js +48 -0
  134. package/src/development/Inspection.js +104 -0
  135. package/src/development/ObservedRenderer.js +42 -0
  136. package/src/development/description.js +35 -0
  137. package/src/development/loopbackRequest.js +27 -0
  138. package/src/development/refreshBrowser.js +96 -0
  139. package/src/development/refreshStyles.js +9 -0
  140. package/src/development/settings.js +17 -0
  141. package/src/docs/Documentation.js +182 -0
  142. package/src/htmx/ActionDefinition.js +44 -0
  143. package/src/htmx/Jsx.js +24 -8
  144. package/src/htmx/LiveHtmlServer.js +41 -19
  145. package/src/htmx/LivePage.js +63 -13
  146. package/src/htmx/PageIdentity.js +32 -0
  147. package/src/htmx/PageLifetime.js +37 -0
  148. package/src/htmx/PageManager.js +203 -74
  149. package/src/htmx/ReactiveRenderer.js +241 -0
  150. package/src/htmx/StaticExporter.js +1 -1
  151. package/src/htmx/TemplateRenderer.js +13 -7
  152. package/src/htmx/browserRuntime.js +2 -93
  153. package/src/htmx/metadata.js +19 -7
  154. package/src/validation/ActionInputError.js +12 -0
  155. package/src/validation/SchemaValidator.js +38 -0
  156. package/src/ws/AdmissionPolicy.js +24 -23
  157. package/src/ws/BaseSocketServer.js +53 -38
  158. package/src/ws/ContractValidationError.js +12 -0
  159. package/src/ws/HeartbeatMonitor.js +19 -7
  160. package/src/ws/ProtocolPolicy.js +1 -1
  161. package/src/ws/RoomAccess.js +82 -0
  162. package/src/ws/RoomRegistry.js +56 -6
  163. package/src/ws/RouteRuntime.js +56 -10
  164. package/src/ws/SocketContract.js +112 -0
  165. package/src/ws/SocketRoute.js +18 -0
  166. package/src/ws/protocol-schema.json +6 -1
  167. package/examples/live-html/chatroom.ts +0 -217
@@ -0,0 +1,1207 @@
1
+ {
2
+ "api": [
3
+ {
4
+ "id": "httpserver",
5
+ "name": "HttpServer",
6
+ "type": "HTTP",
7
+ "summary": "Wraps Express with sensible defaults (JSON body parsing, CORS, and static asset folders) and starts listening immediately unless `listen: false` is supplied. You get the underlying Express instance back via `app`.",
8
+ "usage": "const { HttpServer, METHODS } = require('redweb')\n\nconst server = new HttpServer({\n port: 4000,\n bind: '0.0.0.0',\n publicPaths: ['./static'],\n services: [\n { serviceName: '/ping', method: METHODS.GET, function: (req, res) => res.json({ pong: true }) },\n ],\n})\n\n// Express is still available:\nserver.app.get('/health', (req, res) => res.send('ok'))",
9
+ "options": [
10
+ "port: number (default 80)",
11
+ "bind: string (default 0.0.0.0)",
12
+ "publicPaths: string[] (default [\"./public\"])",
13
+ "services: array of { serviceName, method, function }",
14
+ "listen: boolean (default true); set false to build app without binding a port",
15
+ "listenCallback: function invoked after listen",
16
+ "encoding: \"json\" | \"urlencoded\" (default json)",
17
+ "corsOptions: passed to cors"
18
+ ],
19
+ "methods": [
20
+ {
21
+ "name": "constructor(options)",
22
+ "detail": "Merges defaults, wires body parsing, CORS, static serving, registers REST services, and starts listening unless listen is false."
23
+ },
24
+ {
25
+ "name": "app (Express instance)",
26
+ "detail": "Use the returned `app` to add middleware or routes exactly like a normal Express server."
27
+ }
28
+ ],
29
+ "article": {
30
+ "eli5": "Think of HttpServer as a furnished storefront: Express is the building, while Redweb installs the front door, service counter, signs, and sensible safety rails before you open.",
31
+ "useWhen": "Choose it when Redweb should own a normal HTTP listener and you still want direct access to Express for middleware or one-off routes.",
32
+ "walkthrough": [
33
+ "Redweb creates the Express application and installs the configured parsers, CORS policy, static folders, and services.",
34
+ "The server binds to the requested interface unless listen is false.",
35
+ "The app property remains the same Express application, so adding /health does not require a Redweb abstraction."
36
+ ],
37
+ "watchFor": "Use listen: false when another object must own the Node listener; two owners trying to bind the same port is an application design error."
38
+ }
39
+ },
40
+ {
41
+ "id": "basehttpserver",
42
+ "name": "BaseHttpServer",
43
+ "type": "HTTP",
44
+ "summary": "Public Express app builder used by HttpServer and HttpsServer. Use it for advanced composition when you want Redweb middleware, static files, and services without any listener behavior.",
45
+ "usage": "const { BaseHttpServer, METHODS } = require('redweb')\n\nconst base = new BaseHttpServer({\n publicPaths: ['./public'],\n services: [\n { serviceName: '/health', method: METHODS.GET, function: (req, res) => res.json({ ok: true }) },\n ],\n})\n\nbase.app.get('/extra', (req, res) => res.send('ok'))",
46
+ "options": [
47
+ "All `HttpServer` app-building options",
48
+ "server: existing Express application to configure",
49
+ "listen is ignored because BaseHttpServer never binds a port"
50
+ ],
51
+ "methods": [
52
+ {
53
+ "name": "constructor(options)",
54
+ "detail": "Builds or configures an Express app with body parsing, CORS, static serving, and REST services."
55
+ },
56
+ {
57
+ "name": "app (Express instance)",
58
+ "detail": "The configured Express application. Pass it to http.createServer(app) for custom server ownership."
59
+ }
60
+ ],
61
+ "article": {
62
+ "eli5": "BaseHttpServer prepares the kitchen but does not open the restaurant. You get a fully arranged Express app and decide which Node server will serve it.",
63
+ "useWhen": "Use it for custom composition, tests, serverless adapters, or any setup where creating and listening on the HTTP server belongs to your application.",
64
+ "walkthrough": [
65
+ "The constructor configures either your Express app or a new one.",
66
+ "Static folders and service definitions are registered in deterministic order.",
67
+ "You pass base.app to http.createServer, a test harness, or another host when you are ready."
68
+ ],
69
+ "watchFor": "It intentionally ignores listener settings. If requests are not arriving, confirm that your own server is listening and forwarding them to base.app."
70
+ }
71
+ },
72
+ {
73
+ "id": "httpsserver",
74
+ "name": "HttpsServer",
75
+ "type": "HTTP",
76
+ "summary": "TLS-enabled variant of `HttpServer`. Accepts `ssl.key` and `ssl.cert` file paths, wraps them in an https server, and bootstraps the same middleware pipeline.",
77
+ "usage": "const { HttpsServer, METHODS } = require('redweb')\n\nnew HttpsServer({\n port: 4443,\n ssl: { key: './certs/dev.key', cert: './certs/dev.crt' },\n services: [\n { serviceName: '/secure', method: METHODS.GET, function: (req, res) => res.json({ ok: true }) },\n ],\n})",
78
+ "options": [
79
+ "All `HttpServer` options",
80
+ "ssl.key: path to private key (required)",
81
+ "ssl.cert: path to certificate (required)"
82
+ ],
83
+ "methods": [
84
+ {
85
+ "name": "constructor(options)",
86
+ "detail": "Loads the provided key/cert pair, builds the Express app with BaseHttpServer, then creates and starts the HTTPS listener unless listen is false."
87
+ }
88
+ ],
89
+ "article": {
90
+ "eli5": "HttpsServer is HttpServer with a locked, encrypted front door. It reads your certificate and key, then serves the same Express application through TLS.",
91
+ "useWhen": "Use it when the Node process terminates TLS itself instead of sitting behind a reverse proxy or managed load balancer.",
92
+ "walkthrough": [
93
+ "The key and certificate files are loaded before the listener starts.",
94
+ "The standard HTTP middleware and services are attached to the Express app.",
95
+ "The resulting HTTPS server binds on the configured port and handles encrypted requests."
96
+ ],
97
+ "watchFor": "Certificate rotation, filesystem permissions, and secure protocol policy remain deployment concerns. Behind a TLS-terminating proxy, HttpServer is usually simpler."
98
+ }
99
+ },
100
+ {
101
+ "id": "socketserver",
102
+ "name": "SocketServer",
103
+ "type": "WebSocket",
104
+ "summary": "HTTP-upgrade WebSocket server on top of `ws`. Builds and listens on its own HTTP server by default; if you pass a Node `server`, it attaches upgrade handling and leaves `.listen()` to you unless `listen: true` is explicit.",
105
+ "usage": "const http = require('http')\nconst { HttpServer, METHODS, SocketServer } = require('redweb')\n\nconst httpServer = new HttpServer({\n listen: false,\n publicPaths: ['./public'],\n services: [\n { serviceName: '/health', method: METHODS.GET, function: (req, res) => res.json({ ok: true }) },\n ],\n})\n\nconst server = http.createServer(httpServer.app)\n\nnew SocketServer({\n server,\n routes: [ChatRoute],\n})\n\nserver.listen(3030)",
106
+ "options": [
107
+ "port: number (default 3000)",
108
+ "listen: boolean (default true for owned servers); supplied servers do not listen unless explicitly true",
109
+ "server: existing http.Server to attach to without double-listening (optional)",
110
+ "routes: array of SocketRoute subclasses (defaults to a single DefaultRoute at \"/\")"
111
+ ],
112
+ "methods": [
113
+ {
114
+ "name": "constructor(options)",
115
+ "detail": "Creates or reuses an HTTP server, instantiates supplied routes or a DefaultRoute, attaches upgrade handling, and starts listening only when Redweb owns the server or listen is explicitly true."
116
+ },
117
+ {
118
+ "name": "addRoute(RouteClass)",
119
+ "detail": "Instantiate and register another `SocketRoute` at runtime."
120
+ },
121
+ {
122
+ "name": "handleUpgrade(req, socket, head)",
123
+ "detail": "Internal: normalises the request path, picks a matching route (or \"/\"), and forwards the upgrade to that route's server."
124
+ },
125
+ {
126
+ "name": "shutdown()",
127
+ "detail": "Closes all registered routes and the underlying HTTP server."
128
+ }
129
+ ],
130
+ "article": {
131
+ "eli5": "SocketServer is a switchboard for persistent conversations. It accepts a WebSocket upgrade, finds the route for that path, and lets that route handle the connection.",
132
+ "useWhen": "Choose it for ws:// endpoints or when a reverse proxy already handles TLS and you need one or more independently configured socket routes.",
133
+ "walkthrough": [
134
+ "The HTTP server is built or reused according to the options.",
135
+ "Upgrade requests are matched to routes instead of being sent to every handler.",
136
+ "Each route owns its clients, handlers, services, limits, and cleanup lifecycle."
137
+ ],
138
+ "watchFor": "When supplying an existing Node server, Redweb does not assume it should call listen. Make listener ownership explicit and shut down in the reverse order of startup."
139
+ }
140
+ },
141
+ {
142
+ "id": "securesocketserver",
143
+ "name": "SecureSocketServer",
144
+ "type": "WebSocket",
145
+ "summary": "HTTPS + WebSocket pairing. Mirrors `SocketServer` but wraps an HTTPS server built from the provided TLS files.",
146
+ "usage": "const { SecureSocketServer } = require('redweb')\nconst { GameRoute } = require('./routes/GameRoute')\n\nnew SecureSocketServer({\n port: 3443,\n ssl: { key: './certs/dev.key', cert: './certs/dev.crt' },\n routes: [GameRoute],\n})",
147
+ "options": [
148
+ "port: number (default 3000)",
149
+ "listen: boolean (default true for owned servers); supplied servers do not listen unless explicitly true",
150
+ "server: existing https.Server to attach to without double-listening (optional)",
151
+ "ssl.key and ssl.cert: required file paths",
152
+ "routes: array of SocketRoute subclasses"
153
+ ],
154
+ "methods": [
155
+ {
156
+ "name": "constructor(options)",
157
+ "detail": "Loads TLS files or reuses a supplied HTTPS server, registers the provided routes, attaches upgrade handling, and starts listening only when Redweb owns the server or listen is explicitly true."
158
+ },
159
+ {
160
+ "name": "addRoute(RouteClass)",
161
+ "detail": "Same runtime route attachment as `SocketServer`."
162
+ },
163
+ {
164
+ "name": "shutdown()",
165
+ "detail": "Stops routes, services, and the HTTPS listener."
166
+ }
167
+ ],
168
+ "article": {
169
+ "eli5": "SecureSocketServer is the encrypted version of the WebSocket switchboard: clients use wss:// and the connection stays protected from the first handshake onward.",
170
+ "useWhen": "Use it when Redweb directly owns a TLS WebSocket listener rather than sharing an externally terminated HTTPS connection.",
171
+ "walkthrough": [
172
+ "TLS material is loaded to create or configure the HTTPS listener.",
173
+ "WebSocket upgrades travel through the same route selection used by SocketServer.",
174
+ "Route handlers see ordinary Redweb sockets after the secure handshake completes."
175
+ ],
176
+ "watchFor": "Do not duplicate TLS termination accidentally. If a proxy already provides wss:// publicly, attach SocketServer behind it and validate forwarded origin information."
177
+ }
178
+ },
179
+ {
180
+ "id": "socketroute",
181
+ "name": "SocketRoute",
182
+ "type": "WebSocket",
183
+ "summary": "Defines a WebSocket endpoint and owns its handlers, services, clients, and opt-in multiplayer policies. Routes can add bounded admission, transport limits, ordered work, heartbeat, rooms, resumable sessions, distribution, draining, metrics, and protocol negotiation without changing legacy routes.",
184
+ "usage": "const { SocketRoute } = require('redweb')\nconst { ChatHandler } = require('./handlers/ChatHandler')\nconst { ClockService } = require('./services/ClockService')\n\nclass ChatRoute extends SocketRoute {\n constructor() {\n super({\n path: '/chat',\n handlers: [ChatHandler],\n services: [ClockService],\n allowDuplicateConnections: true,\n websocketOptions: {\n maxPayload: 1024 * 1024,\n perMessageDeflate: false,\n },\n })\n }\n}",
185
+ "options": [
186
+ "path: WebSocket path (required)",
187
+ "handlers: array of handler classes (required)",
188
+ "services: array of SocketService subclasses (optional)",
189
+ "allowDuplicateConnections: allow multiple clients from the same IP",
190
+ "websocketOptions: options passed to ws WebSocketServer, such as maxPayload or perMessageDeflate",
191
+ "admission: authenticate, validate origins, and optionally place a client before upgrade",
192
+ "maxPendingUpgrades: finite concurrent admission/negotiation work (default 64)",
193
+ "limits: connection, message-rate, pending-message, and outbound-buffer ceilings",
194
+ "orderedMessages: serialize each connection through a bounded queue",
195
+ "heartbeat: one route-level half-open connection monitor",
196
+ "rooms and sessions: bounded grouping and expiring application-issued ownership",
197
+ "distribution: optional bounded broker adapter; no broker is bundled or required",
198
+ "drainHandlers: track handler work and expose a cooperative shutdown signal",
199
+ "protocol: version negotiation, stable envelopes/error codes, and optional binary codecs"
200
+ ],
201
+ "methods": [
202
+ {
203
+ "name": "constructor({ path, handlers, services, allowDuplicateConnections, websocketOptions })",
204
+ "detail": "Validates input, instantiates handlers/services, sets up a `ws` server for the path with any websocketOptions, and registers connection listeners."
205
+ },
206
+ {
207
+ "name": "addHandler(HandlerClass)",
208
+ "detail": "Adds another handler class unless a handler with the same name already exists."
209
+ },
210
+ {
211
+ "name": "handleConnection(socket, req)",
212
+ "detail": "Stores the client (deduping by IP unless allowed), decorates the socket with `sendJson`/`broadcast`, and wires close/error/message listeners."
213
+ },
214
+ {
215
+ "name": "handleMessage(socket, data)",
216
+ "detail": "Parses JSON text frames, finds the handler matching `data.type`; on success delegates to handler.handleMessage, otherwise replies with an error and closes the socket."
217
+ },
218
+ {
219
+ "name": "handleBinaryMessage(socket, buffer)",
220
+ "detail": "Handles binary frames separately from JSON text frames and delegates to a handler selected by acceptsBinary(socket, buffer)."
221
+ },
222
+ {
223
+ "name": "handleClose(socket, ip)",
224
+ "detail": "Removes the client from the registry and triggers an optional `connectionCloseCallback`."
225
+ },
226
+ {
227
+ "name": "shutdown()",
228
+ "detail": "Marks the route draining, stops services, bounds handler/adapter cleanup, closes clients, and releases every route-owned resource."
229
+ },
230
+ {
231
+ "name": "beginDrain()",
232
+ "detail": "Flips readiness and rejects new upgrades before shutdown work begins."
233
+ },
234
+ {
235
+ "name": "isReady()",
236
+ "detail": "Reports whether the route is accepting upgrades and any required distribution adapter is healthy."
237
+ },
238
+ {
239
+ "name": "publish(type, payload)",
240
+ "detail": "Publishes through the optional bounded distribution adapter and resolves to a success boolean."
241
+ },
242
+ {
243
+ "name": "handleError(socket, error, ip)",
244
+ "detail": "Logs socket errors; override for custom reporting."
245
+ }
246
+ ],
247
+ "article": {
248
+ "eli5": "A SocketRoute is a room with its own door and rules. The URL chooses the room; message.type chooses which handler inside the room receives the message.",
249
+ "useWhen": "Create one whenever a WebSocket path represents a distinct protocol, trust boundary, workload, or group of multiplayer resources.",
250
+ "walkthrough": [
251
+ "The /match path selects the route during the WebSocket upgrade.",
252
+ "Admission and capacity checks run before the connection becomes an active client.",
253
+ "Messages are dispatched by type to BaseHandler instances while route services and registries share the same lifecycle."
254
+ ],
255
+ "watchFor": "Keep routing decisions out of message.action branches. Prefer one route per protocol area and one handler per message type."
256
+ }
257
+ },
258
+ {
259
+ "id": "socketservice",
260
+ "name": "SocketService",
261
+ "type": "WebSocket",
262
+ "summary": "Route-scoped background worker. Used by `SocketRoute` to run ticks or lifecycle hooks tied to a specific route.",
263
+ "usage": "const { SocketService } = require('redweb')\n\nclass ClockService extends SocketService {\n constructor() { super('clock', 1000) }\n onTick() {\n this.route.clients.forEach((socket) => socket.sendJson({ type: 'time', now: Date.now() }))\n }\n}",
264
+ "methods": [
265
+ {
266
+ "name": "constructor(name, tickRateMs = null)",
267
+ "detail": "Stores a service id and optional tick interval; the interval is activated in onInit if onTick exists."
268
+ },
269
+ {
270
+ "name": "onInit(route)",
271
+ "detail": "Called once by SocketRoute, sets `this.route` and, if a tick interval was supplied, schedules recurring onTick execution."
272
+ },
273
+ {
274
+ "name": "onTick()",
275
+ "detail": "Optional; implement to run on the configured interval."
276
+ },
277
+ {
278
+ "name": "onShutdown()",
279
+ "detail": "Clears the tick interval; extend for cleanup hooks."
280
+ }
281
+ ],
282
+ "article": {
283
+ "eli5": "A SocketService is a helper that clocks in when its route starts and clocks out when the route stops, such as presence tracking or a periodic snapshot publisher.",
284
+ "useWhen": "Use it for route-scoped background behavior that needs explicit startup, shutdown, and access to the owning route.",
285
+ "walkthrough": [
286
+ "The service is constructed with a stable name.",
287
+ "SocketRoute starts it once the route is ready.",
288
+ "Shutdown awaits the service so timers, subscriptions, and external connections cannot leak."
289
+ ],
290
+ "watchFor": "Every resource acquired in start must have a bounded and idempotent release in stop. Avoid detached timers or promises that outlive the route."
291
+ }
292
+ },
293
+ {
294
+ "id": "fixedstepservice",
295
+ "name": "FixedStepService",
296
+ "type": "Multiplayer",
297
+ "summary": "Route-scoped simulation clock that compensates for drift, prevents overlapping async ticks, bounds catch-up work, and reports dropped retained lag instead of replaying forever.",
298
+ "usage": "const { FixedStepService } = require('redweb')\n\nclass Simulation extends FixedStepService {\n constructor() { super('simulation', 50, 3) }\n async onTick(stepMs, tick) {\n await game.update(stepMs, tick)\n }\n}",
299
+ "methods": [
300
+ {
301
+ "name": "constructor(name, tickRateMs, maxCatchUpTicks?, maxRetainedLagMs?)",
302
+ "detail": "Creates a fixed-step route service with finite catch-up and retained-lag limits."
303
+ },
304
+ {
305
+ "name": "onTick(stepMs, tick)",
306
+ "detail": "Implement one simulation step. Async work never overlaps the next pulse."
307
+ },
308
+ {
309
+ "name": "onLagDropped(milliseconds)",
310
+ "detail": "Optional observability hook called when retained lag is deliberately discarded."
311
+ }
312
+ ],
313
+ "article": {
314
+ "eli5": "FixedStepService is a metronome for game logic. Even when the computer hesitates, it advances the simulation in measured beats without starting two beats at once.",
315
+ "useWhen": "Use it for authoritative simulations or periodic work that needs stable step sizes, bounded catch-up, and explicit handling of excessive lag.",
316
+ "walkthrough": [
317
+ "The service schedules ticks using the configured fixed interval.",
318
+ "A slow asynchronous tick finishes before another begins.",
319
+ "Limited catch-up reduces drift, while old excess lag is reported and discarded instead of causing an endless spiral."
320
+ ],
321
+ "watchFor": "A fixed step does not make expensive work free. Measure onTick duration, set conservative catch-up limits, and keep network I/O outside the critical simulation path."
322
+ }
323
+ },
324
+ {
325
+ "id": "roomregistry",
326
+ "name": "RoomRegistry",
327
+ "type": "Multiplayer",
328
+ "summary": "Bounded route-local connection groups. Sockets normally use joinRoom, leaveRoom, and roomBroadcast; disconnect cleanup removes all memberships and reclaims empty rooms.",
329
+ "usage": "class MatchRoute extends SocketRoute {\n constructor() {\n super({\n path: '/match',\n handlers: [JoinMatchHandler, MoveMatchHandler, ResumeMatchHandler],\n rooms: { maxRooms: 1000, maxMembersPerRoom: 32 },\n })\n }\n}",
330
+ "methods": [
331
+ {
332
+ "name": "socket.joinRoom(roomId)",
333
+ "detail": "Idempotently joins a bounded room and returns whether membership is active."
334
+ },
335
+ {
336
+ "name": "socket.leaveRoom(roomId)",
337
+ "detail": "Idempotently leaves one room and reclaims it when empty."
338
+ },
339
+ {
340
+ "name": "socket.roomBroadcast(roomId, data, options?)",
341
+ "detail": "Serializes once and sends to selected connected members."
342
+ }
343
+ ],
344
+ "article": {
345
+ "eli5": "RoomRegistry is a set of labeled group chats. A socket can join a label, and one message can be delivered to everyone carrying that label.",
346
+ "useWhen": "Use rooms for match participants, parties, regions, spectators, or any bounded route-local fan-out group.",
347
+ "walkthrough": [
348
+ "A join handler adds the socket to the requested match room.",
349
+ "The move handler broadcasts the accepted update to that room and can exclude the sender.",
350
+ "Disconnect cleanup removes every membership and empty rooms are reclaimed automatically."
351
+ ],
352
+ "watchFor": "A room is a delivery group, not authoritative game state. Validate membership and permissions before broadcasting, and configure hard room and membership limits."
353
+ }
354
+ },
355
+ {
356
+ "id": "sessionregistry",
357
+ "name": "SessionRegistry",
358
+ "type": "Multiplayer",
359
+ "summary": "Bounded, expiring ownership records for application-issued opaque session IDs. Redweb handles takeover and expiry; the application owns credential issuance and payload validation.",
360
+ "usage": "class MatchRoute extends SocketRoute {\n constructor() {\n super({\n path: '/match',\n handlers: [JoinMatchHandler, MoveMatchHandler, ResumeMatchHandler],\n sessions: { ttlMs: 30000, maxSessions: 10000 },\n })\n }\n}",
361
+ "methods": [
362
+ {
363
+ "name": "socket.createSession(sessionId, data)",
364
+ "detail": "Creates a bounded application-issued session owned by the current connection."
365
+ },
366
+ {
367
+ "name": "socket.resumeSession(sessionId)",
368
+ "detail": "Atomically transfers ownership, closes the former owner, and returns stored data."
369
+ },
370
+ {
371
+ "name": "stop()",
372
+ "detail": "Stops the one route-level sweep timer and clears every retained record."
373
+ }
374
+ ],
375
+ "article": {
376
+ "eli5": "SessionRegistry is a numbered coat-check ticket. A reconnecting player presents the ticket and safely takes ownership of the stored session from an older connection.",
377
+ "useWhen": "Use it for short reconnect windows, controlled connection takeover, and small pieces of application-issued resumable state.",
378
+ "walkthrough": [
379
+ "The application creates an opaque session ID and stores bounded state against the connected socket.",
380
+ "After a disconnect, the record remains available for the configured TTL.",
381
+ "Resume atomically transfers ownership and closes the former owner if it is still connected."
382
+ ],
383
+ "watchFor": "Session IDs are credentials: issue them securely, never trust client-selected identity, limit stored data, and persist important state outside this in-memory registry."
384
+ }
385
+ },
386
+ {
387
+ "id": "protocolclient",
388
+ "name": "ProtocolClient",
389
+ "type": "Client",
390
+ "summary": "Dependency-free helper from redweb/client for opt-in versioned routes. It builds, sends, and validates stable envelopes from the same checked-in schema used by server constants and TypeScript declarations.",
391
+ "usage": "const { ProtocolClient, ERROR_CODES } = require('redweb/client')\n\nconst socket = new WebSocket('wss://game.example/match?redwebVersion=1')\nconst client = new ProtocolClient(socket, '1')\nclient.send('move', { x: 4, y: 2 }, { sequence: 17 })",
392
+ "methods": [
393
+ {
394
+ "name": "constructor(socket, version)",
395
+ "detail": "Wraps any socket-like object with send(data) and selects the envelope version."
396
+ },
397
+ {
398
+ "name": "envelope(type, payload, metadata?)",
399
+ "detail": "Builds a stable versioned event with optional requestId and sequence."
400
+ },
401
+ {
402
+ "name": "send(type, payload, metadata?)",
403
+ "detail": "Serializes and sends one versioned event through the wrapped socket."
404
+ },
405
+ {
406
+ "name": "parse(value)",
407
+ "detail": "Parses and validates a protocol event or error envelope."
408
+ }
409
+ ],
410
+ "article": {
411
+ "eli5": "ProtocolClient is a phrasebook shared with the browser. It puts outgoing messages into Redweb’s expected envelope and checks incoming envelopes before your code trusts them.",
412
+ "useWhen": "Use it for opt-in versioned routes when browser clients should share protocol constants, parsing, sequencing, and error handling with the server.",
413
+ "walkthrough": [
414
+ "The client connects to the versioned route and creates a ProtocolClient for that version.",
415
+ "send builds a stable typed envelope containing payload and optional sequence metadata.",
416
+ "parse validates server messages so application code can respond to known errors such as rate limiting."
417
+ ],
418
+ "watchFor": "Protocol validation is not domain validation. Continue checking payload shape, authorization, and game rules on the authoritative server."
419
+ }
420
+ },
421
+ {
422
+ "id": "socketregistry",
423
+ "name": "SocketRegistry",
424
+ "type": "WebSocket",
425
+ "summary": "Small EventEmitter-backed list for socket-scoped entities (players, rooms, etc.). Emits `added` and `removed` events.",
426
+ "usage": "const { SocketRegistry } = require('redweb')\n\nclass PlayerRegistry extends SocketRegistry {\n addPlayer(player) {\n this.add(player)\n this.emit('playerJoined', player)\n }\n}",
427
+ "methods": [
428
+ {
429
+ "name": "add(item)",
430
+ "detail": "Stores the item and emits an `added` event."
431
+ },
432
+ {
433
+ "name": "remove(itemOrId, by = \"id\")",
434
+ "detail": "Remove by object reference or by matching a property (defaults to \"id\"); returns true when removal occurred and emits `removed`."
435
+ },
436
+ {
437
+ "name": "all()",
438
+ "detail": "Returns a shallow copy of all stored items."
439
+ },
440
+ {
441
+ "name": "count()",
442
+ "detail": "Convenience getter for `all().length`."
443
+ }
444
+ ],
445
+ "article": {
446
+ "eli5": "SocketRegistry is the route’s attendance sheet. It knows which sockets are active and provides a controlled way to visit or disconnect them.",
447
+ "useWhen": "Use it when route logic needs bounded connection tracking, fan-out, observability, or coordinated draining.",
448
+ "walkthrough": [
449
+ "Accepted connections are registered once and removed during close cleanup.",
450
+ "Iteration works over the route-owned set rather than an application-global list.",
451
+ "Shutdown can stop admissions, notify clients, and close the remaining registry deterministically."
452
+ ],
453
+ "watchFor": "Do not retain sockets in parallel collections without cleanup. Prefer rooms or socket context for indexes with clear ownership."
454
+ }
455
+ },
456
+ {
457
+ "id": "basehandler",
458
+ "name": "BaseHandler",
459
+ "type": "WebSocket",
460
+ "summary": "Abstract message handler. Provide a name in the constructor; clients send `{ type: name, ... }` to target JSON messages, while binary frames can be accepted and handled as raw Buffer payloads.",
461
+ "usage": "const { BaseHandler } = require('redweb')\n\nclass UploadHandler extends BaseHandler {\n constructor() { super('upload') }\n\n onMessage(socket, message) {\n socket.sendJson({ type: 'upload:control', action: message.action })\n }\n\n acceptsBinary(socket, buffer) {\n return buffer.length > 0\n }\n\n onBinaryMessage(socket, buffer) {\n socket.sendJson({ type: 'upload:chunk', bytes: buffer.length })\n }\n}",
462
+ "methods": [
463
+ {
464
+ "name": "constructor(name)",
465
+ "detail": "Stores the handler name used by incoming messages."
466
+ },
467
+ {
468
+ "name": "handleMessage(socket, message)",
469
+ "detail": "Calls onMessage; override only if you need pre/post handling logic."
470
+ },
471
+ {
472
+ "name": "onMessage(socket, message)",
473
+ "detail": "Required; implement your message processing here. Throwing will close the socket with an error."
474
+ },
475
+ {
476
+ "name": "acceptsBinary(socket, buffer)",
477
+ "detail": "Optional selector used by SocketRoute to choose a handler for binary frames. Return true when this handler should receive the Buffer."
478
+ },
479
+ {
480
+ "name": "handleBinaryMessage(socket, buffer)",
481
+ "detail": "Calls onBinaryMessage. If onBinaryMessage is not implemented, Redweb sends a \"Binary messages are not supported by this handler\" error."
482
+ },
483
+ {
484
+ "name": "onBinaryMessage(socket, buffer)",
485
+ "detail": "Optional; override for normal binary-message handling. The second argument is the raw Buffer payload."
486
+ },
487
+ {
488
+ "name": "onInitialContact(socket)",
489
+ "detail": "Optional hook for first-touch logic (not used by default route)."
490
+ }
491
+ ],
492
+ "article": {
493
+ "eli5": "BaseHandler is a labeled mailbox. A message with the matching type goes directly into that mailbox, so your code does not need a switch statement.",
494
+ "useWhen": "Create a small handler for each message type that deserves its own validation, authorization, rate policy, and behavior.",
495
+ "walkthrough": [
496
+ "The handler name declares the message type it accepts.",
497
+ "SocketRoute performs dispatch before onMessage runs.",
498
+ "The handler validates the payload, changes authoritative state, and sends or broadcasts the result."
499
+ ],
500
+ "watchFor": "Do not add a second message.action dispatcher inside one handler. That hides protocol operations and defeats type-based routing."
501
+ }
502
+ },
503
+ {
504
+ "id": "sendjson",
505
+ "name": "sendJson",
506
+ "type": "Utility",
507
+ "summary": "Utility to JSON.stringify data and send it over a `ws` socket.",
508
+ "usage": "const { sendJson } = require('redweb')\n\nsendJson(socket, { type: 'ping' })",
509
+ "methods": [
510
+ {
511
+ "name": "sendJson(socket, data)",
512
+ "detail": "Serialises data and writes it to the socket."
513
+ }
514
+ ],
515
+ "article": {
516
+ "eli5": "sendJson is a careful packer: give it a JavaScript value and it turns that value into one JSON message before placing it on the socket.",
517
+ "useWhen": "Use it for structured server-to-client messages instead of repeating JSON.stringify and transport checks throughout handlers.",
518
+ "walkthrough": [
519
+ "Application code creates a normal object with a type and payload.",
520
+ "sendJson serializes it once using the route’s safe sending path.",
521
+ "The client receives one complete text frame and parses the matching JSON value."
522
+ ],
523
+ "watchFor": "Serialization can fail on cycles and BigInt values, and large objects still consume memory. Bound payload sizes and keep messages purpose-specific."
524
+ }
525
+ },
526
+ {
527
+ "id": "errorcodes",
528
+ "name": "ERROR_CODES",
529
+ "type": "Constants",
530
+ "summary": "Stable framework error codes shared by protocol-enabled servers and redweb/client.",
531
+ "usage": "const { ERROR_CODES } = require('redweb')\n\n// INVALID_MESSAGE, UNKNOWN_HANDLER, HANDLER_FAILED,\n// BINARY_UNSUPPORTED, RATE_LIMITED, QUEUE_FULL,\n// CAPACITY_REACHED, INITIALIZATION_FAILED",
532
+ "methods": [
533
+ {
534
+ "name": "Message errors",
535
+ "detail": "INVALID_MESSAGE, UNKNOWN_HANDLER, HANDLER_FAILED, and BINARY_UNSUPPORTED."
536
+ },
537
+ {
538
+ "name": "Capacity errors",
539
+ "detail": "RATE_LIMITED, QUEUE_FULL, and CAPACITY_REACHED."
540
+ },
541
+ {
542
+ "name": "Lifecycle errors",
543
+ "detail": "INITIALIZATION_FAILED."
544
+ }
545
+ ],
546
+ "article": {
547
+ "eli5": "ERROR_CODES is a shared list of machine-readable reasons, like standardized traffic signs that every client interprets the same way.",
548
+ "useWhen": "Use these constants whenever application behavior depends on a Redweb protocol failure rather than human-facing wording.",
549
+ "walkthrough": [
550
+ "The server emits a stable code inside its error envelope.",
551
+ "The client compares it with ERROR_CODES instead of copying a string literal.",
552
+ "UI or retry policy can change independently of the readable error message."
553
+ ],
554
+ "watchFor": "Codes describe protocol outcomes, not every domain failure. Add your own namespaced application codes without changing Redweb’s meanings."
555
+ }
556
+ },
557
+ {
558
+ "id": "socketoptions",
559
+ "name": "SOCKET_OPTIONS",
560
+ "type": "Constants",
561
+ "summary": "Default WebSocket server options used by BaseSocketServer.",
562
+ "usage": "const { SOCKET_OPTIONS } = require('redweb')\n// { port: 3000, ssl: null, listen: true, routes: [] }",
563
+ "methods": [
564
+ {
565
+ "name": "port",
566
+ "detail": "Default WebSocket port (3000)."
567
+ },
568
+ {
569
+ "name": "ssl",
570
+ "detail": "Default TLS config (null)."
571
+ },
572
+ {
573
+ "name": "listen",
574
+ "detail": "Owned socket servers listen by default. Supplied servers remain caller-owned unless listen is explicitly true."
575
+ },
576
+ {
577
+ "name": "routes",
578
+ "detail": "Routes array default (empty; a DefaultRoute is created when none are provided)."
579
+ }
580
+ ],
581
+ "article": {
582
+ "eli5": "SOCKET_OPTIONS is the default settings card Redweb starts from before applying the socket choices you provide.",
583
+ "useWhen": "Read it to understand defaults or build tooling that presents Redweb configuration, but pass explicit options for production decisions.",
584
+ "walkthrough": [
585
+ "Redweb begins with immutable documented defaults.",
586
+ "User configuration overrides only the supplied fields.",
587
+ "The normalized result is used consistently when the socket server starts."
588
+ ],
589
+ "watchFor": "Treat exported defaults as documentation, not mutable global configuration. Never change the object to configure one server."
590
+ }
591
+ },
592
+ {
593
+ "id": "httpoptions",
594
+ "name": "HTTP_OPTIONS",
595
+ "type": "Constants",
596
+ "summary": "Frozen defaults used by the HTTP and HTTPS server constructors.",
597
+ "usage": "const { HTTP_OPTIONS } = require('redweb')\n// port 80, bind 0.0.0.0, publicPaths ['./public'],\n// listen true, encoding 'json', and safe error exposure disabled",
598
+ "methods": [
599
+ {
600
+ "name": "Server defaults",
601
+ "detail": "port, bind, listen, ssl, logger, and static public paths."
602
+ },
603
+ {
604
+ "name": "Application defaults",
605
+ "detail": "services, encoding, CORS, and safe error exposure."
606
+ }
607
+ ],
608
+ "article": {
609
+ "eli5": "HTTP_OPTIONS is Redweb’s starter checklist for HTTP servers: port, bind address, public folders, encoding, and related defaults.",
610
+ "useWhen": "Consult it when you need to know what omitted HttpServer options mean or when generating configuration documentation.",
611
+ "walkthrough": [
612
+ "The server copies its baseline HTTP choices.",
613
+ "Your provided values are validated and merged.",
614
+ "The completed configuration drives app creation and optional listener startup."
615
+ ],
616
+ "watchFor": "Defaults are convenient locally but production networking should be explicit, especially bind address, port, CORS, and listener ownership."
617
+ }
618
+ },
619
+ {
620
+ "id": "encodings",
621
+ "name": "ENCODINGS",
622
+ "type": "Constants",
623
+ "summary": "Supported request-body parser names for HTTP server configuration.",
624
+ "usage": "const { ENCODINGS } = require('redweb')\n// ENCODINGS.json, ENCODINGS.urlencoded",
625
+ "methods": [
626
+ {
627
+ "name": "json",
628
+ "detail": "Use Express JSON body parsing."
629
+ },
630
+ {
631
+ "name": "urlencoded",
632
+ "detail": "Use Express URL-encoded body parsing."
633
+ }
634
+ ],
635
+ "article": {
636
+ "eli5": "ENCODINGS is a tiny menu that lets you choose whether request bodies arrive as JSON or traditional URL-encoded form data.",
637
+ "useWhen": "Use the constants when configuring HTTP body parsing so spelling stays aligned with supported Redweb values.",
638
+ "walkthrough": [
639
+ "The selected constant is passed in HttpServer options.",
640
+ "Redweb installs the corresponding Express body parser.",
641
+ "Route handlers then read the parsed value from req.body."
642
+ ],
643
+ "watchFor": "Body parsing is not schema validation. Limit body size and validate every field before using it."
644
+ }
645
+ },
646
+ {
647
+ "id": "methods",
648
+ "name": "METHODS",
649
+ "type": "Constants",
650
+ "summary": "Lowercase HTTP verb helpers passed straight to Express route registration.",
651
+ "usage": "const { METHODS } = require('redweb')\n// METHODS.GET, METHODS.POST, METHODS.PUT, METHODS.DELETE",
652
+ "methods": [
653
+ {
654
+ "name": "GET",
655
+ "detail": "Use with services array or Express: METHODS.GET"
656
+ },
657
+ {
658
+ "name": "POST",
659
+ "detail": "Use with services array or Express: METHODS.POST"
660
+ },
661
+ {
662
+ "name": "PUT",
663
+ "detail": "Use with services array or Express: METHODS.PUT"
664
+ },
665
+ {
666
+ "name": "DELETE",
667
+ "detail": "Use with services array or Express: METHODS.DELETE"
668
+ }
669
+ ],
670
+ "article": {
671
+ "eli5": "METHODS is a spelling-safe list of HTTP verbs—the labels on requests that say whether they read, create, replace, change, or delete something.",
672
+ "useWhen": "Use it in Redweb service definitions to avoid scattered uppercase strings and accidental unsupported verbs.",
673
+ "walkthrough": [
674
+ "A service definition pairs its path with a METHODS value.",
675
+ "BaseHttpServer registers the matching Express operation.",
676
+ "Requests with another verb do not accidentally invoke that service."
677
+ ],
678
+ "watchFor": "The HTTP verb is only one part of API semantics. Implement authentication, idempotency, validation, and appropriate status codes in the service."
679
+ }
680
+ },
681
+ {
682
+ "id": "basesocketserver",
683
+ "name": "BaseSocketServer",
684
+ "type": "WebSocket",
685
+ "summary": "Shared lifecycle and route-composition base for SocketServer and SecureSocketServer. Extend the concrete servers for normal applications; use this type when building infrastructure integrations.",
686
+ "usage": "import { BaseSocketServer } from 'redweb'\n\n// SocketServer and SecureSocketServer inherit:\n// addRoute(), handleUpgrade(), beginDrain(), and shutdown().",
687
+ "methods": [
688
+ {
689
+ "name": "addRoute(RouteClass)",
690
+ "detail": "Creates and registers one route class while enforcing unique paths."
691
+ },
692
+ {
693
+ "name": "beginDrain()",
694
+ "detail": "Stops accepting new upgrades across every registered route."
695
+ },
696
+ {
697
+ "name": "shutdown()",
698
+ "detail": "Drains routes and closes owned listeners without closing caller-owned servers."
699
+ }
700
+ ],
701
+ "article": {
702
+ "eli5": "BaseSocketServer is the engine room beneath both plain and secure socket servers. It coordinates routes and upgrades without deciding how the outer listener was created.",
703
+ "useWhen": "Use this advanced surface for custom integrations that need Redweb routing on a specially managed Node HTTP or HTTPS server.",
704
+ "walkthrough": [
705
+ "Your application supplies or prepares the listener.",
706
+ "BaseSocketServer attaches bounded WebSocket upgrade and route lifecycle behavior.",
707
+ "Concrete server ownership remains visible, including whether Redweb may listen or close it."
708
+ ],
709
+ "watchFor": "Most applications should use SocketServer or SecureSocketServer. Reach for the base class only when listener ownership cannot be expressed by their options."
710
+ }
711
+ },
712
+ {
713
+ "id": "livehtmlserver",
714
+ "name": "LiveHtmlServer",
715
+ "type": "Live HTML",
716
+ "summary": "Decorator-first server rendering and realtime browser updates on Redweb’s existing HTTP and WebSocket stack. Pages can be connection-scoped or intentionally shared.",
717
+ "usage": "import { LiveHtmlServer } from 'redweb'\nimport { DocsPage, StatusPage } from './pages.js'\n\nconst server = new LiveHtmlServer({\n pages: [DocsPage, StatusPage],\n port: 8080,\n heartbeat: { intervalMs: 15_000, timeoutMs: 10_000 },\n})",
718
+ "options": [
719
+ "pages: non-empty array of classes decorated with page()",
720
+ "templateRoot: optional root for colocated HTML and CSS assets",
721
+ "sessionTtlMs and maxSessions: bound pending and reconnectable sessions",
722
+ "maxConcurrentRenders: independent HTTP render concurrency ceiling",
723
+ "heartbeat: detects half-open browser connections",
724
+ "authenticate: binds HTTP renders and socket upgrades to one stable identity",
725
+ "origins: exact allowlist or asynchronous origin predicate"
726
+ ],
727
+ "methods": [
728
+ {
729
+ "name": "constructor(options)",
730
+ "detail": "Builds the page renderer, HTTP routes, generated assets, and live WebSocket route on one listener."
731
+ },
732
+ {
733
+ "name": "shutdown()",
734
+ "detail": "Aborts active renders, drains routes, disposes pages and components, and closes owned resources."
735
+ }
736
+ ],
737
+ "article": {
738
+ "eli5": "LiveHtmlServer is a stage manager for server-rendered pages. It serves the first complete HTML scene, then carries approved actions backstage and sends updated pieces back.",
739
+ "useWhen": "Use it for decorator-first HTML applications that need server state and realtime interaction without React, hydration, or a separate client API layer.",
740
+ "walkthrough": [
741
+ "HTTP rendering creates complete HTML and binds a stable page identity.",
742
+ "The generated live socket accepts only declared actions for that page.",
743
+ "State changes produce bounded updates while authentication and resource limits cover both transports."
744
+ ],
745
+ "watchFor": "Decide deliberately whether page state is per connection or shared. Authenticate both HTTP and upgrade paths with the same identity and cap render concurrency."
746
+ }
747
+ },
748
+ {
749
+ "id": "livepage",
750
+ "name": "LivePage and start",
751
+ "type": "Live HTML",
752
+ "summary": "A page is an ordinary decorated class; extending LivePage is optional. start() is the concise entry point that creates a LiveHtmlServer for one or more page classes.",
753
+ "methods": [
754
+ {
755
+ "name": "start(PageClass, options?)",
756
+ "detail": "Starts one decorated page, or an array of pages, with the concise Live HTML server API."
757
+ },
758
+ {
759
+ "name": "loading(context)",
760
+ "detail": "Optional cancellable hook that runs before the initial server render."
761
+ },
762
+ {
763
+ "name": "connected(context)",
764
+ "detail": "Optional hook that runs after the authenticated live socket connects."
765
+ },
766
+ {
767
+ "name": "disconnected(context)",
768
+ "detail": "Optional hook for stopping connection-owned timers and subscriptions."
769
+ },
770
+ {
771
+ "name": "disposed()",
772
+ "detail": "Optional idempotent final cleanup hook for pages and components."
773
+ }
774
+ ],
775
+ "article": {
776
+ "eli5": "LivePage is one server-owned screen; start is the power button that publishes your collection of screens and their realtime connection.",
777
+ "useWhen": "Use a LivePage class when a route owns state, actions, lifecycle, and rendered output; use start to launch the assembled application.",
778
+ "walkthrough": [
779
+ "The page decorator assigns the HTTP route and rendering metadata.",
780
+ "A new page instance is created according to its configured scope.",
781
+ "start builds the HTTP and socket surfaces, then returns a handle for orderly shutdown."
782
+ ],
783
+ "watchFor": "Keep page constructors cheap and move cancellable preparation into lifecycle hooks. Always retain and await the returned shutdown handle."
784
+ },
785
+ "recipe": {
786
+ "template": "realtime",
787
+ "file": "src/app.tsx"
788
+ },
789
+ "language": "tsx"
790
+ },
791
+ {
792
+ "id": "livedecorators",
793
+ "name": "page, component, state, action, view",
794
+ "type": "Live HTML",
795
+ "summary": "Small TypeScript decorators declare routes, reusable component ownership, reactive server state, browser-callable actions, and collection item views.",
796
+ "methods": [
797
+ {
798
+ "name": "page(path, options?)",
799
+ "detail": "Registers a unique route plus template, CSS, sharing, metadata, caching, and live/static behavior."
800
+ },
801
+ {
802
+ "name": "component()",
803
+ "detail": "Marks a class as a reusable state/action/lifecycle namespace."
804
+ },
805
+ {
806
+ "name": "component(render)",
807
+ "detail": "Creates a concise synchronous function component for stateless reusable HTML."
808
+ },
809
+ {
810
+ "name": "state(options?)",
811
+ "detail": "Publishes reassigned values; writable state may also receive bounded browser input."
812
+ },
813
+ {
814
+ "name": "action({ input? })",
815
+ "detail": "Explicitly exposes one method to rw-click or rw-submit. An optional Standard Schema input validates and transforms one submitted argument before invocation; ActionInput<typeof schema> describes its output. Invalid input stays recoverable, while validator bugs remain server failures. Undecorated methods stay unreachable."
816
+ },
817
+ {
818
+ "name": "rw-status=\"action\"",
819
+ "detail": "Optional component-scoped placement for built-in action feedback. Without a slot, buttons/forms get an automatic status message. Pending duplicates from one control are suppressed; late responses preserve changed drafts and replacement forms. Disconnected actions are never queued or replayed."
820
+ },
821
+ {
822
+ "name": "view(stateName)",
823
+ "detail": "Renders each item of one decorated array for an rw-each collection."
824
+ }
825
+ ],
826
+ "article": {
827
+ "eli5": "The decorators are small labels: page says where a screen lives, component says what can be reused, state says what may change, action says what the browser may request, and view describes repeated items.",
828
+ "useWhen": "Use them to make server-rendered ownership visible next to the class member it affects instead of maintaining a separate routing and binding manifest.",
829
+ "walkthrough": [
830
+ "@page registers the outer route and page policy.",
831
+ "@state and @action expose only explicitly declared reactive behavior.",
832
+ "The counter renders ordinary TSX over its state; changes reach both tabs because the page explicitly opts into shared state. Use the chat recipe to explore reusable class components."
833
+ ],
834
+ "watchFor": "Decorators are an allow-list, not decoration. Keep actions narrow, validate their arguments, and avoid exposing arbitrary method invocation."
835
+ },
836
+ "recipe": {
837
+ "template": "realtime",
838
+ "file": "src/app.tsx"
839
+ },
840
+ "language": "tsx"
841
+ },
842
+ {
843
+ "id": "jsxruntime",
844
+ "name": "JSX rendering",
845
+ "type": "Live HTML",
846
+ "summary": "Dependency-free server-side TSX that renders directly to HtmlFragment values. It provides readable components, fragments, arrays, automatic escaping, safe attributes, and existing html-fragment interoperability without React, a virtual DOM, or hydration.",
847
+ "usage": "// tsconfig.json\n// { \"compilerOptions\": { \"jsx\": \"react-jsx\", \"jsxImportSource\": \"redweb\" } }\n\nimport { component, page } from 'redweb'\nimport type { Child } from 'redweb/jsx-runtime'\n\nconst Card = component((props: { title: string; children?: Child }) => (\n <article class=\"card\">\n <h2>{props.title}</h2>\n {props.children}\n </article>\n))\n\n@page('/docs', { css: 'docs.css', live: false })\nclass DocsPage {\n render() {\n return <main><Card title=\"Redweb\">Readable server TSX</Card></main>\n }\n}",
848
+ "options": [
849
+ "TypeScript: jsx = react-jsx and jsxImportSource = redweb",
850
+ "Production runtime: redweb/jsx-runtime; development runtime: redweb/jsx-dev-runtime",
851
+ "External CSS and rw-* server directives replace inline styles and browser event functions"
852
+ ],
853
+ "methods": [
854
+ {
855
+ "name": "Intrinsic elements",
856
+ "detail": "Serialize standard, SVG, custom, data-*, aria-*, and rw-* attributes with HTML-correct boolean handling."
857
+ },
858
+ {
859
+ "name": "Fragments and arrays",
860
+ "detail": "Compose nested fragments and readonly child arrays without wrapper markup or comma coercion."
861
+ },
862
+ {
863
+ "name": "Function components",
864
+ "detail": "Synchronous functions receive typed props and children and must return an HtmlFragment or fragment array."
865
+ },
866
+ {
867
+ "name": "Escaping and URLs",
868
+ "detail": "Text and attributes escape automatically; URL attributes retain Redweb’s safe-protocol validation."
869
+ },
870
+ {
871
+ "name": "Interoperability",
872
+ "detail": "Existing html fragments nest in TSX and TSX fragments nest in html for incremental migration."
873
+ }
874
+ ],
875
+ "article": {
876
+ "eli5": "Redweb JSX is a readable HTML-shaped pencil. It turns TSX into safe server HTML directly—there is no React engine or browser copy hiding behind it.",
877
+ "useWhen": "Use it whenever nested template strings become difficult to read or reusable server-rendered components make page structure clearer.",
878
+ "walkthrough": [
879
+ "TypeScript sends JSX calls to redweb/jsx-runtime.",
880
+ "Text and attributes are escaped while fragments, arrays, and components are flattened predictably.",
881
+ "The resulting HtmlFragment works with static pages and Live HTML without hydration."
882
+ ],
883
+ "watchFor": "JSX is syntax, not automatic client reactivity. Only Live HTML state and actions create a realtime channel; static-site TSX remains zero-runtime HTML."
884
+ }
885
+ },
886
+ {
887
+ "id": "safehtml",
888
+ "name": "html, attribute, url, each, codeBlock",
889
+ "type": "Live HTML",
890
+ "summary": "Safe composition primitives escape text and quoted primitive attributes by default. URL attributes additionally reject executable, protocol-relative, and malformed values. Arrays must contain trusted HtmlFragment values.",
891
+ "usage": "import { codeBlock, each, html } from 'redweb'\n\nconst links = sections.map(section => ({\n id: section.id,\n href: `#${section.id}`,\n label: section.name,\n}))\n\nconst navigation = each(links, link => html`\n <a id=\"${link.id}\" href=\"${link.href}\">${link.label}</a>\n`)\n\nconst example = codeBlock(source, {\n language: 'ts',\n label: 'TypeScript',\n highlight: highlightTypeScript,\n})",
892
+ "methods": [
893
+ {
894
+ "name": "html`...`",
895
+ "detail": "Creates an HtmlFragment and escapes every ordinary interpolation."
896
+ },
897
+ {
898
+ "name": "attribute(value)",
899
+ "detail": "Optionally brands a primitive for a quoted, non-URL attribute when explicit intent improves readability."
900
+ },
901
+ {
902
+ "name": "url(value)",
903
+ "detail": "Optionally brands a safe relative, HTTP, HTTPS, mail, or telephone URL; direct strings receive the same validation."
904
+ },
905
+ {
906
+ "name": "each(items, render)",
907
+ "detail": "Validates and joins a mutable or readonly list of HtmlFragment results."
908
+ },
909
+ {
910
+ "name": "codeBlock(code, options?)",
911
+ "detail": "Builds an escaped figure/pre/code fragment and can invoke a safe server-side highlighter."
912
+ }
913
+ ],
914
+ "article": {
915
+ "eli5": "These helpers are different safety tools: html builds trusted structure, attribute and url escape risky contexts, each joins lists, and codeBlock displays code without executing it.",
916
+ "useWhen": "Use them for low-level templates, dynamic attributes, URLs, collections, and code samples when JSX is not the clearest representation.",
917
+ "walkthrough": [
918
+ "Untrusted values are passed through the helper matching their HTML context.",
919
+ "HtmlFragment values preserve the distinction between approved markup and ordinary text.",
920
+ "Collection and code helpers produce predictable escaped output without hand-built concatenation."
921
+ ],
922
+ "watchFor": "Escaping is context-specific. Never treat an escaped attribute as a safe URL or mark user-provided HTML as trusted."
923
+ }
924
+ },
925
+ {
926
+ "id": "definesite",
927
+ "name": "defineSite",
928
+ "type": "Live HTML",
929
+ "summary": "Defines shared static-site CSS, metadata, caching, layout, canonical URLs, and export behavior once. Site pages are always runtime-free.",
930
+ "usage": "import { defineSite } from 'redweb'\n\nconst docs = defineSite({\n origin: 'https://example.com',\n css: 'site.css',\n head: { description: 'Product documentation', image: '/og.png' },\n cache: { maxAge: 300 },\n layout: content => <body><nav>Product</nav><main>{content}</main></body>,\n})\n\n@docs.page('/docs', { head: { title: 'Documentation' } })\nclass DocsPage {\n render() { return <h1>Documentation</h1> }\n}\n\nawait docs.export(DocsPage, {\n outDir: 'dist',\n publicDir: 'public',\n})",
931
+ "options": [
932
+ "origin: optional HTTP(S) origin used to derive canonical and root-relative social-image URLs",
933
+ "css, head, cache, and layout: defaults inherited by every site.page() decorator",
934
+ "layout: synchronous function receiving the trusted page fragment and portable request context",
935
+ "publicDir: optional link-free asset tree staged with generated output"
936
+ ],
937
+ "methods": [
938
+ {
939
+ "name": "site.page(path, options?)",
940
+ "detail": "Creates a non-live page decorator while merging shared defaults and page-specific overrides."
941
+ },
942
+ {
943
+ "name": "site.export(pageOrPages, options)",
944
+ "detail": "Stages all pages and public assets, rejects path collisions, then writes the destination and returns every output path."
945
+ }
946
+ ],
947
+ "article": {
948
+ "eli5": "defineSite is the shared blueprint for a collection of pages: one place for the frame, colors, metadata, cache policy, canonical links, and export rules.",
949
+ "useWhen": "Use it for documentation, marketing, or content sites where many runtime-free pages should share layout and production behavior.",
950
+ "walkthrough": [
951
+ "The site definition establishes origin, common CSS, head metadata, cache policy, and layout.",
952
+ "Decorated pages contribute their own route, title, description, and stylesheet.",
953
+ "site.export stages every page and asset into one consistent static output."
954
+ ],
955
+ "watchFor": "The layout must handle every exported route. Keep route-specific chrome derived from stable metadata rather than scattered path checks."
956
+ }
957
+ },
958
+ {
959
+ "id": "exportstatic",
960
+ "name": "exportStatic",
961
+ "type": "Live HTML",
962
+ "summary": "Renders non-live decorated pages to deterministic directory indexes and content-addressed CSS. It is intended for docs, marketing pages, and static hosting.",
963
+ "usage": "import path from 'node:path'\nimport { exportStatic, page } from 'redweb'\n\n@page('/docs', { live: false, css: 'docs.css' })\nclass DocsPage {\n render() { return '<h1>Redweb docs</h1>' }\n}\n\nawait exportStatic(DocsPage, {\n outDir: path.resolve('dist'),\n templateRoot: path.resolve('src/pages'),\n})",
964
+ "options": [
965
+ "outDir: required output directory; existing unrelated files are preserved",
966
+ "templateRoot: optional explicit root for templates and CSS",
967
+ "logger: optional framework logger or null"
968
+ ],
969
+ "methods": [
970
+ {
971
+ "name": "exportStatic(pageOrPages, options)",
972
+ "detail": "Returns frozen page and asset path lists after every page renders and its CSS is written."
973
+ }
974
+ ],
975
+ "article": {
976
+ "eli5": "exportStatic is a printing press: give it pages, and it writes finished HTML and assets that any ordinary static host can serve.",
977
+ "useWhen": "Use it when you need direct static export without defining a reusable site object and its shared layout policy.",
978
+ "walkthrough": [
979
+ "Page metadata determines output routes and page assets.",
980
+ "Every page renders on the server into a staging destination.",
981
+ "The returned paths let build tooling audit or publish exactly what was produced."
982
+ ],
983
+ "watchFor": "Export into a staging directory and replace the live build atomically. A failed render should never leave a partially updated site."
984
+ }
985
+ },
986
+ {
987
+ "id": "htmlrenderer",
988
+ "name": "HtmlRenderer",
989
+ "type": "Live HTML",
990
+ "summary": "Lower-level rendering utility behind Live HTML. Most applications should use page(), start(), defineSite(), and exportStatic(); this surface supports advanced integrations and tooling.",
991
+ "usage": "import { HtmlRenderer } from 'redweb'\n\nconst markup = HtmlRenderer.render(\n '<h1>{{ title }}</h1>',\n { title: 'Reference' },\n { live: false },\n)\n\nconst document = HtmlRenderer.document(markup, null, [], {\n title: 'Reference',\n})",
992
+ "methods": [
993
+ {
994
+ "name": "render(source, page, options?)",
995
+ "detail": "Renders declarative bindings and collection views against a page object."
996
+ },
997
+ {
998
+ "name": "document(markup, config?, stylesheets?, metadata?)",
999
+ "detail": "Wraps markup in a complete document and injects metadata, CSS, and optional live bootstrap."
1000
+ },
1001
+ {
1002
+ "name": "template() / stylesheet()",
1003
+ "detail": "Loads a validated page asset inside an explicit root."
1004
+ },
1005
+ {
1006
+ "name": "statePayload()",
1007
+ "detail": "Builds the text or trusted-HTML state payload used by live updates."
1008
+ }
1009
+ ],
1010
+ "article": {
1011
+ "eli5": "HtmlRenderer is the machinery under the hood that turns a page object and its declared bindings into final markup.",
1012
+ "useWhen": "Use it for tooling or advanced integrations that genuinely need lower-level rendering control; normal applications should prefer pages, start, defineSite, or exportStatic.",
1013
+ "walkthrough": [
1014
+ "A source template or fragment and page instance enter the renderer.",
1015
+ "Declared state, actions, views, and safe values are resolved under the render options.",
1016
+ "The caller receives final HTML suitable for its own response or build pipeline."
1017
+ ],
1018
+ "watchFor": "The lower-level API gives you more lifecycle responsibility. Preserve escaping, cancellation, ownership, and bounded concurrency rather than rebuilding them casually."
1019
+ }
1020
+ },
1021
+ {
1022
+ "id": "socketcontract",
1023
+ "name": "defineSocketContract",
1024
+ "type": "WebSocket",
1025
+ "language": "ts",
1026
+ "summary": "One shared Standard Schema contract validates wire payloads and infers client/server types. Route URLs choose the service; individual handler factories dispatch by message type.",
1027
+ "recipe": {
1028
+ "template": "socket",
1029
+ "file": "src/contract.ts"
1030
+ },
1031
+ "methods": [
1032
+ {
1033
+ "name": "defineSocketContract(version, schemas, options?)",
1034
+ "detail": "Creates an immutable contract and negotiated protocol policy from Standard Schema validators. Redweb does not require a particular validator at runtime."
1035
+ },
1036
+ {
1037
+ "name": "handler(type, callback)",
1038
+ "detail": "Creates a BaseHandler subclass. Validation completes before the callback receives its typed payload."
1039
+ },
1040
+ {
1041
+ "name": "client(socket)",
1042
+ "detail": "Wraps an existing WebSocket with typed, validated send and parse methods. It does not open or reconnect the connection."
1043
+ },
1044
+ {
1045
+ "name": "send(socket, type, payload, metadata?)",
1046
+ "detail": "Validates server output before sending through the normal transport and protocol policy."
1047
+ }
1048
+ ],
1049
+ "article": {
1050
+ "eli5": "A socket contract is a shared form for both sides of a conversation. It says which messages exist and what each message must contain, so the server checks a message before handing it to the matching handler.",
1051
+ "useWhen": "Use it when a site, app, or game client should share payload types and runtime validation with a routed Redweb service.",
1052
+ "walkthrough": [
1053
+ "The shared module defines join, move, resume, and state payload schemas once.",
1054
+ "The match route registers separate handlers created by the contract instead of switching on a secondary action field.",
1055
+ "The client and server validate their outgoing messages and parse incoming envelopes against the same schema."
1056
+ ],
1057
+ "watchFor": "Schema validation is not authentication or game-rule validation. Keep bearer session IDs private, apply authorization in handlers, and persist important data outside the starter's bounded in-memory sessions. Async validation deadlines cannot preempt synchronous JavaScript."
1058
+ }
1059
+ }
1060
+ ],
1061
+ "examples": [
1062
+ {
1063
+ "id": "room-access",
1064
+ "label": "Private rooms",
1065
+ "language": "tsx",
1066
+ "title": "One identity for a page and a protected room",
1067
+ "summary": "A complete local demonstration of shared authentication, decorator-first server HTML and explicitly authorized raw socket room entry.",
1068
+ "notes": [
1069
+ "Save this as src/app.tsx in an initialized realtime starter, build and start it. The printed token is a fresh local-demo credential; do not publish it or treat this as a production identity service.",
1070
+ "Supply the Authorization header for GET / and the /team WebSocket, then send {\"type\":\"join\"}. Browser products should use a secure session/cookie integration; native browser WebSockets cannot set this header.",
1071
+ "Both decorator modes and source-free production execution are checked with real HTTP/WebSockets. The example explicitly revokes raw room memberships as well as Live HTML page sessions."
1072
+ ],
1073
+ "codeSource": "docs/snippets/room-access.tsx"
1074
+ },
1075
+ {
1076
+ "id": "shared-server",
1077
+ "label": "Start here",
1078
+ "title": "HTTP and WebSockets on one listener",
1079
+ "summary": "Build the Express side without binding, attach route classes to the same Node server, and explicitly give the socket service responsibility for listening and cleanup.",
1080
+ "language": "tsx",
1081
+ "notes": [
1082
+ "GET /health returns JSON; ws://127.0.0.1:8181/chat accepts {\"type\":\"hello\"}. The HTTP endpoint reports liveness, not readiness.",
1083
+ "Use the complete http-ws starter for compiler configuration, the shared entrypoint helper, and actual HTTP/WebSocket tests. It binds loopback for local development.",
1084
+ "The socket service explicitly owns shared-listener cleanup with closeServerOnShutdown: true. Call its shutdown() rather than a second HTTP shutdown sequence. Configure authentication, trusted origins, limits, and HTTPS/WSS before public deployment."
1085
+ ],
1086
+ "recipe": { "template": "http-ws", "file": "src/app.tsx" }
1087
+ },
1088
+ {
1089
+ "id": "live-html",
1090
+ "label": "Rendered UI",
1091
+ "language": "tsx",
1092
+ "title": "Server state and reusable TSX components",
1093
+ "summary": "Ordinary TSX expressions read server-owned state and update automatically after an action; no repeated binding names or browser component runtime.",
1094
+ "notes": [
1095
+ "The first response is complete server-rendered HTML.",
1096
+ "Function components handle stateless snippets; decorated classes own state and actions.",
1097
+ "There is no React dependency, virtual DOM, or hydration pass."
1098
+ ],
1099
+ "recipe": {
1100
+ "template": "realtime",
1101
+ "file": "src/app.tsx"
1102
+ }
1103
+ },
1104
+ {
1105
+ "id": "static-site",
1106
+ "label": "Static HTML",
1107
+ "language": "tsx",
1108
+ "title": "A complete site with shared defaults",
1109
+ "summary": "Define metadata, layout, CSS, caching, and asset export once, then keep each page focused on its content.",
1110
+ "notes": [
1111
+ "Canonical URLs are derived from the route and origin.",
1112
+ "Public assets and rendered pages are staged before output changes.",
1113
+ "The generated site contains no Redweb browser runtime or WebSocket."
1114
+ ],
1115
+ "code": "import { defineSite } from 'redweb'\n\nconst site = defineSite({\n origin: 'https://example.com',\n css: 'site.css',\n head: { description: 'Product documentation', image: '/og.png' },\n cache: { maxAge: 300 },\n layout: content => <body><nav>Product</nav><main>{content}</main></body>,\n})\n\n@site.page('/docs', { head: { title: 'Documentation' } })\nclass DocsPage {\n render() { return <h1>Documentation</h1> }\n}\n\nawait site.export(DocsPage, {\n outDir: 'dist',\n publicDir: 'public',\n})"
1116
+ },
1117
+ {
1118
+ "id": "handlers",
1119
+ "label": "Messages",
1120
+ "title": "JSON routing, broadcast, and binary frames",
1121
+ "summary": "Text messages select a handler by type. Binary frames stay as Buffer values and can be accepted by the handler that understands them.",
1122
+ "notes": [
1123
+ "sendJson and broadcast share the same outbound policy.",
1124
+ "acceptsBinary can select among multiple binary handlers.",
1125
+ "Async handler failures become safe client errors."
1126
+ ],
1127
+ "code": "const { BaseHandler, SocketRoute } = require('redweb')\n\nclass ChatHandler extends BaseHandler {\n constructor() { super('chat') }\n\n onMessage(socket, message) {\n socket.broadcast({ type: 'chat', text: message.text })\n }\n}\n\nclass SnapshotHandler extends BaseHandler {\n constructor() { super('snapshot') }\n onMessage() {}\n acceptsBinary(_socket, buffer) { return buffer.length > 0 }\n onBinaryMessage(socket, buffer) {\n socket.sendJson({ type: 'snapshot:received', bytes: buffer.length })\n }\n}\n\nclass RealtimeRoute extends SocketRoute {\n constructor() {\n super({\n path: '/realtime',\n handlers: [ChatHandler, SnapshotHandler],\n websocketOptions: { maxPayload: 64 * 1024 },\n })\n }\n}"
1128
+ },
1129
+ {
1130
+ "id": "protected-route",
1131
+ "label": "Production",
1132
+ "title": "Bound admission, work, and slow peers",
1133
+ "summary": "Production controls are opt-in and route-local. Authenticate before upgrade, cap every queue, and use one heartbeat scheduler for the whole route.",
1134
+ "notes": [
1135
+ "Authentication completes before onInitialContact.",
1136
+ "Ordered overflow closes pending work synchronously.",
1137
+ "Disabled controls add no per-connection queue or timer."
1138
+ ],
1139
+ "code": "class MatchRoute extends SocketRoute {\n constructor() {\n super({\n path: '/match',\n handlers: [InputHandler],\n admission: {\n origins: ['https://game.example'],\n timeoutMs: 3000,\n authenticate: (request, { signal }) =>\n verifyPlayer(request, signal),\n },\n maxPendingUpgrades: 64,\n limits: {\n maxConnections: 5000,\n maxBufferedBytes: 1024 * 1024,\n maxPendingMessages: 64,\n messageRate: { capacity: 60, refillPerSecond: 30 },\n },\n orderedMessages: true,\n heartbeat: { intervalMs: 30000, timeoutMs: 10000 },\n websocketOptions: { maxPayload: 64 * 1024 },\n })\n }\n}"
1140
+ },
1141
+ {
1142
+ "id": "rooms-sessions",
1143
+ "label": "Players",
1144
+ "language": "ts",
1145
+ "title": "Match handlers and resumable ownership",
1146
+ "summary": "Give the match its own socket route, then dispatch join, move and resume by type. These canonical socket-starter handlers create and recover server-owned player sessions; they are not a room-broadcast or account-authentication example.",
1147
+ "notes": [
1148
+ "Initialize the complete socket recipe: src/contract.ts defines validated payloads and src/app.tsx configures /match, session capacity and transport limits. This file is not a standalone server.",
1149
+ "Join issues a random bearer session token. Move requires an existing player; resume restores that player on a new connection and replaces the previous owner.",
1150
+ "Keep the state response and session token private. Add account authentication and application movement rules before production; sessions remain in memory and expire 30 seconds after disconnect.",
1151
+ "For authenticated group delivery, use the separate One identity for a page and a protected room example. The shared socket contracts guide links its complete source and the room-authorization guide."
1152
+ ],
1153
+ "recipe": {
1154
+ "template": "socket",
1155
+ "file": "src/handlers.ts"
1156
+ }
1157
+ },
1158
+ {
1159
+ "id": "fixed-step",
1160
+ "label": "Simulation",
1161
+ "title": "Fixed-step work without overlapping ticks",
1162
+ "summary": "FixedStepService compensates for scheduler drift, bounds catch-up, contains async failures, and reports lag that was deliberately dropped.",
1163
+ "notes": [
1164
+ "The active async tick must finish before another begins.",
1165
+ "maxCatchUpTicks prevents a spiral of death.",
1166
+ "maxRetainedLagMs bounds remembered delay."
1167
+ ],
1168
+ "code": "const { FixedStepService, SocketRoute } = require('redweb')\n\nclass Simulation extends FixedStepService {\n constructor() {\n super('simulation', 50, 3, 250)\n }\n\n async onTick(stepMs, tick) {\n await authoritativeGame.update(stepMs, tick)\n }\n\n onLagDropped(milliseconds) {\n console.warn('Simulation lag discarded', { milliseconds })\n }\n}\n\nclass SimulationRoute extends SocketRoute {\n constructor() {\n super({\n path: '/simulation',\n handlers: [InputHandler],\n services: [Simulation],\n })\n }\n}"
1169
+ },
1170
+ {
1171
+ "id": "protocol",
1172
+ "label": "Clients",
1173
+ "title": "Versioned envelopes and the dependency-free client",
1174
+ "summary": "Negotiate a finite protocol version before upgrade, then share stable envelopes and error codes between server and client.",
1175
+ "notes": [
1176
+ "Browsers negotiate with redwebVersion in the query.",
1177
+ "requestId correlates; sequence expresses application ordering.",
1178
+ "Neither field promises durability or exactly-once delivery."
1179
+ ],
1180
+ "code": "const { SocketRoute } = require('redweb')\n\nclass ProtocolRoute extends SocketRoute {\n constructor() {\n super({\n path: '/match',\n handlers: [MoveHandler],\n protocol: {\n versions: ['2', '1'],\n binary: {\n maxBytes: 64 * 1024,\n encode: (state) => codec.encode(state),\n decode: (bytes) => codec.decode(bytes),\n },\n },\n })\n }\n}\n\n// Browser client\nconst { ProtocolClient, ERROR_CODES } = require('redweb/client')\nconst socket = new WebSocket(\n 'wss://game.example/match?redwebVersion=2'\n)\nconst client = new ProtocolClient(socket, '2')\n\nsocket.addEventListener('message', (event) => {\n const message = client.parse(event)\n if (message.error?.code === ERROR_CODES.RATE_LIMITED) backOff()\n})\n\nclient.send('move', { x: 4, y: 2 }, { sequence: 17 })"
1181
+ },
1182
+ {
1183
+ "id": "distribution",
1184
+ "label": "Multiple nodes",
1185
+ "title": "Bring your own broker adapter",
1186
+ "summary": "Redweb supplies a bounded composition seam rather than choosing infrastructure. Events are finite, deduplicated briefly, and explicitly best-effort.",
1187
+ "notes": [
1188
+ "Required adapters affect readiness; best-effort adapters do not.",
1189
+ "Source-node events are ignored to prevent reflection loops.",
1190
+ "Authoritative state and partition reconciliation remain application work."
1191
+ ],
1192
+ "code": "const { SocketRoute } = require('redweb')\n\nclass DistributedMatchRoute extends SocketRoute {\n constructor() {\n super({\n path: '/match',\n handlers: [JoinMatchHandler, MoveMatchHandler, ResumeMatchHandler],\n rooms: true,\n distribution: {\n adapter: brokerAdapter,\n channel: 'matches',\n nodeId: process.env.INSTANCE_ID,\n required: true,\n maxEventBytes: 64 * 1024,\n maxConcurrentPublishes: 32,\n onEvent(event, route) {\n route.rooms.broadcast(event.payload.roomId, {\n type: event.type,\n payload: event.payload,\n })\n },\n },\n })\n }\n}\n\n// From a connected socket:\nawait socket.publishEvent('match:update', update)"
1193
+ },
1194
+ {
1195
+ "id": "draining",
1196
+ "label": "Operations",
1197
+ "title": "Readiness first, then bounded shutdown",
1198
+ "summary": "Stop placement to the node, flip readiness, let cooperative handlers observe cancellation, and await deterministic cleanup.",
1199
+ "notes": [
1200
+ "New upgrades receive 503 once draining starts.",
1201
+ "The route signal is shared through socket.context.signal.",
1202
+ "A hard deadline terminates non-cooperating peers."
1203
+ ],
1204
+ "code": "const { HttpServer, SocketServer } = require('redweb')\n\nconst http = new HttpServer({ listen: false })\nconst socketServer = new SocketServer({\n server: http.server,\n routes: [MatchRoute],\n})\n\nhttp.app.get('/ready', (_request, response) => {\n response.sendStatus(socketServer.isReady() ? 200 : 503)\n})\n\nhttp.server.listen(3000)\n\nprocess.once('SIGTERM', async () => {\n socketServer.beginDrain()\n await stopExternalPlacement()\n await socketServer.shutdown()\n await http.shutdown()\n})\n\n// In a handler with drainHandlers: true\nawait saveCheckpoint({ signal: socket.context.signal })"
1205
+ }
1206
+ ]
1207
+ }