redweb 0.16.2 → 0.16.4

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 (71) hide show
  1. package/CHANGELOG.md +40 -29
  2. package/README.md +293 -291
  3. package/contract.d.ts +11 -11
  4. package/docs/API_EXAMPLES_VERIFICATION.md +22 -22
  5. package/docs/APPLICATION.md +96 -94
  6. package/docs/CLI.md +122 -122
  7. package/docs/CLIENT_DEVELOPMENT.md +9 -9
  8. package/docs/CONNECTED_CLIENTS_VERIFICATION.md +65 -65
  9. package/docs/DEVELOPMENT.md +81 -81
  10. package/docs/GETTING_STARTED.md +78 -78
  11. package/docs/LIVE_HTML.md +555 -478
  12. package/docs/MIGRATION.md +28 -28
  13. package/docs/RELEASE_TRUST.md +88 -88
  14. package/docs/RUNTIME_DIAGNOSTICS.md +78 -78
  15. package/docs/SOCKET_CONTRACTS.md +42 -42
  16. package/docs/SOCKET_PAGES.md +172 -172
  17. package/docs/SOCKET_PAGE_RELEASE_PREPARATION.md +120 -120
  18. package/docs/SOCKET_PAGE_VERIFICATION.md +85 -85
  19. package/docs/generated.json +2290 -2286
  20. package/docs/guides/chatroom.md +1 -1
  21. package/docs/guides/jsx-without-react.md +14 -14
  22. package/docs/reference.json +1333 -1329
  23. package/docs/releases/0.15.0.json +2217 -2217
  24. package/docs/releases/0.16.0.json +2217 -2217
  25. package/docs/releases/0.16.1.json +2286 -2286
  26. package/docs/releases/0.16.2.json +2286 -2286
  27. package/docs/releases/0.16.3.json +2286 -0
  28. package/docs/releases/0.16.4.json +2290 -0
  29. package/docs/snippets/components.tsx +24 -24
  30. package/docs/snippets/counter.tsx +16 -16
  31. package/docs/snippets/room-access.tsx +11 -11
  32. package/docs/snippets/site.css +2 -2
  33. package/docs/snippets/site.tsx +22 -22
  34. package/docs/topics.json +3 -3
  35. package/index.d.ts +96 -58
  36. package/index.js +14 -8
  37. package/package.json +12 -8
  38. package/recipes/foundation/README.md +7 -7
  39. package/recipes/foundation/app.test.cjs +15 -15
  40. package/recipes/foundation/app.tsx +12 -12
  41. package/recipes/shared/README.md +7 -7
  42. package/src/Application.js +4 -4
  43. package/src/access/failure-codes.json +4 -0
  44. package/src/cli/ProjectInitializer.js +1 -1
  45. package/src/cli/arguments.js +21 -21
  46. package/src/cli/run.js +12 -12
  47. package/src/cli/templates.js +40 -40
  48. package/src/docs/Documentation.js +29 -29
  49. package/src/htmx/CodeHighlight.js +98 -0
  50. package/src/htmx/Html.js +4 -3
  51. package/src/htmx/Jsx.js +2 -2
  52. package/src/htmx/LiveHtmlServer.js +5 -1
  53. package/src/htmx/LivePage.js +5 -1
  54. package/src/htmx/LiveResource.js +96 -0
  55. package/src/htmx/PageManager.js +126 -13
  56. package/src/htmx/PageSocketRoute.js +132 -132
  57. package/src/htmx/PageTaskLane.js +39 -0
  58. package/src/htmx/ReactiveRenderer.js +8 -8
  59. package/src/htmx/SocketAction.js +19 -19
  60. package/src/htmx/TemplateRenderer.js +1 -1
  61. package/src/htmx/index.js +4 -2
  62. package/src/htmx/metadata.js +117 -6
  63. package/src/ws/BaseHandler.js +6 -6
  64. package/src/ws/ConnectedClients.js +207 -207
  65. package/src/ws/HandlerGuard.js +4 -4
  66. package/src/ws/RoomRegistry.js +4 -4
  67. package/src/ws/RouteRuntime.js +11 -11
  68. package/src/ws/SocketAction.js +16 -16
  69. package/src/ws/SocketContract.js +3 -3
  70. package/src/ws/SocketRoute.js +7 -7
  71. package/styles/code-highlight.css +16 -0
@@ -1,207 +1,207 @@
1
- 'use strict';
2
-
3
- const { AccessPolicy, AccessDenied } = require('../access/AccessPolicy');
4
- const { guards } = require('./HandlerGuard');
5
- const { BoundedOperation } = require('../async/BoundedOperation');
6
-
7
- /** Deliberately public rejection text; unexpected exceptions remain private. */
8
- class ClientError extends Error {
9
- constructor(message) {
10
- if (typeof message !== 'string' || !message || message.length > 512) throw new TypeError('Client error text must contain 1–512 characters.');
11
- super(message);
12
- }
13
- }
14
-
15
- class ConnectedClient {
16
- #owner;
17
- constructor(owner, socket) {
18
- this.#owner = owner;
19
- Object.defineProperty(this, 'socket', { value: socket });
20
- Object.freeze(this);
21
- }
22
- get identity() { this.#owner.assertActive(this.socket); return this.socket.context.principal; }
23
- get rooms() { return this.#owner.rooms.roomsFor(this.socket); }
24
- get room() {
25
- const rooms = this.rooms;
26
- if (rooms.length !== 1) throw new ClientError('Join a room first.');
27
- return rooms[0];
28
- }
29
- get page() { this.#owner.assertActive(this.socket); return this.socket.page(this.#owner.options.page()); }
30
- join(room, commit) { return this.#owner.join(this, room, commit); }
31
- leave(room) { this.#owner.assertActive(this.socket); return this.#owner.rooms.leave(room, this.socket); }
32
- }
33
-
34
- /** One route-owned projection layer; RoomRegistry alone owns membership. */
35
- class ConnectedClients {
36
- #route;
37
- #clients = new WeakMap();
38
- #joining = new WeakSet();
39
- #queued = new Set();
40
- #generations = new Map();
41
-
42
- constructor(options) {
43
- if (!options || typeof options !== 'object') throw new TypeError('Connected clients require options.');
44
- for (const name of ['identity', 'page', 'project']) {
45
- if (typeof options[name] !== 'function') throw new TypeError(`Connected clients require ${name}.`);
46
- }
47
- for (const name of ['reject', 'errorState']) {
48
- if (options[name] !== undefined && typeof options[name] !== 'function') throw new TypeError(`${name} must be a function.`);
49
- }
50
- if (options.raw !== undefined && (!options.raw || typeof options.raw.update !== 'function' ||
51
- (options.raw.reject !== undefined && typeof options.raw.reject !== 'function'))) {
52
- throw new TypeError('A raw adapter requires update and an optional reject callback.');
53
- }
54
- this.options = Object.freeze({ ...options, raw: options.raw && Object.freeze({ ...options.raw }) });
55
- this.policy = new AccessPolicy(async context => {
56
- const identity = await this.options.identity(context);
57
- return identity !== undefined && identity !== null && identity !== false && Object.is(identity, context.principal);
58
- }, options.authorizationTimeoutMs);
59
- this.projection = new BoundedOperation(options.projectionTimeoutMs);
60
- }
61
-
62
- attach(route, rooms) {
63
- if (this.#route) throw new TypeError('Connected clients belong to one route instance. Create them inside your application factory.');
64
- if (!rooms || !route.protocolPolicy) throw new TypeError('Connected clients require rooms and a socket protocol.');
65
- this.#route = route;
66
- this.rooms = rooms;
67
- }
68
-
69
- assertActive(socket) {
70
- if (!this.#route || this.#route.draining || this.#route.clients.get(socket.clientKey) !== socket ||
71
- socket.readyState !== 1 || socket.context.signal.aborted) throw new AccessDenied('ACCESS_CANCELLED');
72
- }
73
-
74
- get(socket) {
75
- this.assertActive(socket);
76
- let client = this.#clients.get(socket);
77
- if (!client) { client = new ConnectedClient(this, socket); this.#clients.set(socket, client); }
78
- return client;
79
- }
80
-
81
- async check(socket) {
82
- this.assertActive(socket);
83
- await this.policy.check(socket.context);
84
- const guard = guards.get(socket);
85
- if (guard) await guard();
86
- this.assertActive(socket);
87
- if (!socket.page && !this.options.raw) throw new AccessDenied();
88
- }
89
-
90
- async join(client, room, commit = () => {}) {
91
- const socket = client.socket;
92
- if (typeof commit !== 'function') throw new TypeError('Room commit must be synchronous.');
93
- if (this.#joining.has(socket)) throw new ClientError('A room join is already pending.');
94
- const existing = this.rooms.has(room, socket);
95
- this.#joining.add(socket);
96
- try {
97
- await this.check(socket);
98
- if (!await this.rooms.enter(room, socket)) throw new ClientError('Room capacity or access denied.');
99
- await this.check(socket);
100
- if (!this.rooms.has(room, socket)) throw new AccessDenied('ACCESS_CANCELLED');
101
- const result = commit();
102
- if (result && typeof result.then === 'function') {
103
- Promise.resolve(result).catch(() => {});
104
- throw new TypeError('Room commit must be synchronous.');
105
- }
106
- } catch (error) {
107
- if (!existing) this.rooms.leave(room, socket);
108
- throw error;
109
- } finally { this.#joining.delete(socket); this.changed(room); }
110
- }
111
-
112
- changed(room) {
113
- if (this.#route.draining) return;
114
- this.#generations.set(room, Symbol());
115
- if (this.#queued.has(room)) return;
116
- this.#queued.add(room);
117
- queueMicrotask(() => {
118
- if (!this.#queued.delete(room)) return;
119
- void this.refresh(room).catch(error => this.#route.logger.error('Connected client refresh failed:', error));
120
- });
121
- }
122
-
123
- refresh(room) {
124
- this.rooms.validateRoomId(room);
125
- this.#queued.delete(room);
126
- return this.#route.runtime.run(() => this.project(room));
127
- }
128
-
129
- async project(room) {
130
- const generation = Symbol();
131
- this.#generations.set(room, generation);
132
- const current = socket => this.#generations.get(room) === generation && this.rooms.has(room, socket) && !this.#joining.has(socket);
133
- const eligible = [];
134
- try {
135
- await Promise.all(this.rooms.members(room).map(async socket => {
136
- if (!current(socket)) return;
137
- try {
138
- await this.check(socket);
139
- if (current(socket)) { const client = this.get(socket); eligible.push({ client, identity: client.identity }); }
140
- }
141
- catch (error) { if (current(socket)) this.exclude(socket, error); }
142
- }));
143
- const online = Object.freeze([...new Set(eligible.map(entry => entry.identity))]);
144
- await Promise.all(eligible.map(async ({ client }) => {
145
- const socket = client.socket;
146
- try {
147
- if (!current(socket)) return;
148
- const { state, frame } = await this.projection.run(async () => {
149
- const state = await this.options.project(client, room, online);
150
- const frame = socket.page ? null : await this.options.raw.update(state);
151
- return { state, frame };
152
- }, socket.context.signal);
153
- await this.check(socket);
154
- if (!current(socket)) return;
155
- if (socket.page) Object.assign(client.page, state);
156
- else socket.sendEvent(frame.type, frame.payload);
157
- } catch (error) { if (current(socket)) this.exclude(socket, error); }
158
- }));
159
- } finally {
160
- if (this.#generations.get(room) === generation) this.#generations.delete(room);
161
- }
162
- }
163
-
164
- exclude(socket, error) {
165
- this.rooms.leaveAll(socket);
166
- if (!(error instanceof AccessDenied)) this.#route.handleError(socket, error);
167
- socket.close(error instanceof AccessDenied ? 1008 : 1011, 'Connection unavailable.');
168
- }
169
-
170
- bind(contract) {
171
- return Object.freeze({
172
- protocol: contract.protocol,
173
- handler: (type, callback) => {
174
- if (typeof callback !== 'function') throw new TypeError('A client handler requires a callback.');
175
- return contract.handler(type, async (socket, payload, message) => {
176
- const client = this.get(socket);
177
- await this.check(socket);
178
- try {
179
- const result = await callback(client, payload);
180
- if (result === false) return false;
181
- await Promise.all(client.rooms.map(room => this.refresh(room)));
182
- if (!socket.page && message.requestId !== undefined) {
183
- await this.check(socket);
184
- socket.sendEvent('redweb:result', null, { requestId: message.requestId });
185
- }
186
- return result;
187
- } catch (error) {
188
- const text = error instanceof ClientError ? error.message : this.options.reject?.(error);
189
- if (typeof text !== 'string' || !text || text.length > 512) throw error;
190
- const metadata = { requestId: message.requestId };
191
- const frame = socket.page || !this.options.raw.reject ? null :
192
- await this.projection.run(() => this.options.raw.reject(text), socket.context.signal);
193
- await this.check(socket);
194
- if (socket.page) {
195
- if (this.options.errorState) Object.assign(client.page, this.options.errorState(text));
196
- } else if (frame) socket.sendEvent(frame.type, frame.payload);
197
- socket.sendProtocolError('COMMAND_REJECTED', text, metadata);
198
- return false;
199
- }
200
- });
201
- },
202
- });
203
- }
204
- }
205
-
206
- const connectedClients = options => new ConnectedClients(options);
207
- module.exports = { connectedClients, ConnectedClients, ConnectedClient, ClientError };
1
+ 'use strict';
2
+
3
+ const { AccessPolicy, AccessDenied } = require('../access/AccessPolicy');
4
+ const { guards } = require('./HandlerGuard');
5
+ const { BoundedOperation } = require('../async/BoundedOperation');
6
+
7
+ /** Deliberately public rejection text; unexpected exceptions remain private. */
8
+ class ClientError extends Error {
9
+ constructor(message) {
10
+ if (typeof message !== 'string' || !message || message.length > 512) throw new TypeError('Client error text must contain 1–512 characters.');
11
+ super(message);
12
+ }
13
+ }
14
+
15
+ class ConnectedClient {
16
+ #owner;
17
+ constructor(owner, socket) {
18
+ this.#owner = owner;
19
+ Object.defineProperty(this, 'socket', { value: socket });
20
+ Object.freeze(this);
21
+ }
22
+ get identity() { this.#owner.assertActive(this.socket); return this.socket.context.principal; }
23
+ get rooms() { return this.#owner.rooms.roomsFor(this.socket); }
24
+ get room() {
25
+ const rooms = this.rooms;
26
+ if (rooms.length !== 1) throw new ClientError('Join a room first.');
27
+ return rooms[0];
28
+ }
29
+ get page() { this.#owner.assertActive(this.socket); return this.socket.page(this.#owner.options.page()); }
30
+ join(room, commit) { return this.#owner.join(this, room, commit); }
31
+ leave(room) { this.#owner.assertActive(this.socket); return this.#owner.rooms.leave(room, this.socket); }
32
+ }
33
+
34
+ /** One route-owned projection layer; RoomRegistry alone owns membership. */
35
+ class ConnectedClients {
36
+ #route;
37
+ #clients = new WeakMap();
38
+ #joining = new WeakSet();
39
+ #queued = new Set();
40
+ #generations = new Map();
41
+
42
+ constructor(options) {
43
+ if (!options || typeof options !== 'object') throw new TypeError('Connected clients require options.');
44
+ for (const name of ['identity', 'page', 'project']) {
45
+ if (typeof options[name] !== 'function') throw new TypeError(`Connected clients require ${name}.`);
46
+ }
47
+ for (const name of ['reject', 'errorState']) {
48
+ if (options[name] !== undefined && typeof options[name] !== 'function') throw new TypeError(`${name} must be a function.`);
49
+ }
50
+ if (options.raw !== undefined && (!options.raw || typeof options.raw.update !== 'function' ||
51
+ (options.raw.reject !== undefined && typeof options.raw.reject !== 'function'))) {
52
+ throw new TypeError('A raw adapter requires update and an optional reject callback.');
53
+ }
54
+ this.options = Object.freeze({ ...options, raw: options.raw && Object.freeze({ ...options.raw }) });
55
+ this.policy = new AccessPolicy(async context => {
56
+ const identity = await this.options.identity(context);
57
+ return identity !== undefined && identity !== null && identity !== false && Object.is(identity, context.principal);
58
+ }, options.authorizationTimeoutMs);
59
+ this.projection = new BoundedOperation(options.projectionTimeoutMs);
60
+ }
61
+
62
+ attach(route, rooms) {
63
+ if (this.#route) throw new TypeError('Connected clients belong to one route instance. Create them inside your application factory.');
64
+ if (!rooms || !route.protocolPolicy) throw new TypeError('Connected clients require rooms and a socket protocol.');
65
+ this.#route = route;
66
+ this.rooms = rooms;
67
+ }
68
+
69
+ assertActive(socket) {
70
+ if (!this.#route || this.#route.draining || this.#route.clients.get(socket.clientKey) !== socket ||
71
+ socket.readyState !== 1 || socket.context.signal.aborted) throw new AccessDenied('ACCESS_CANCELLED');
72
+ }
73
+
74
+ get(socket) {
75
+ this.assertActive(socket);
76
+ let client = this.#clients.get(socket);
77
+ if (!client) { client = new ConnectedClient(this, socket); this.#clients.set(socket, client); }
78
+ return client;
79
+ }
80
+
81
+ async check(socket) {
82
+ this.assertActive(socket);
83
+ await this.policy.check(socket.context);
84
+ const guard = guards.get(socket);
85
+ if (guard) await guard();
86
+ this.assertActive(socket);
87
+ if (!socket.page && !this.options.raw) throw new AccessDenied();
88
+ }
89
+
90
+ async join(client, room, commit = () => {}) {
91
+ const socket = client.socket;
92
+ if (typeof commit !== 'function') throw new TypeError('Room commit must be synchronous.');
93
+ if (this.#joining.has(socket)) throw new ClientError('A room join is already pending.');
94
+ const existing = this.rooms.has(room, socket);
95
+ this.#joining.add(socket);
96
+ try {
97
+ await this.check(socket);
98
+ if (!await this.rooms.enter(room, socket)) throw new ClientError('Room capacity or access denied.');
99
+ await this.check(socket);
100
+ if (!this.rooms.has(room, socket)) throw new AccessDenied('ACCESS_CANCELLED');
101
+ const result = commit();
102
+ if (result && typeof result.then === 'function') {
103
+ Promise.resolve(result).catch(() => {});
104
+ throw new TypeError('Room commit must be synchronous.');
105
+ }
106
+ } catch (error) {
107
+ if (!existing) this.rooms.leave(room, socket);
108
+ throw error;
109
+ } finally { this.#joining.delete(socket); this.changed(room); }
110
+ }
111
+
112
+ changed(room) {
113
+ if (this.#route.draining) return;
114
+ this.#generations.set(room, Symbol());
115
+ if (this.#queued.has(room)) return;
116
+ this.#queued.add(room);
117
+ queueMicrotask(() => {
118
+ if (!this.#queued.delete(room)) return;
119
+ void this.refresh(room).catch(error => this.#route.logger.error('Connected client refresh failed:', error));
120
+ });
121
+ }
122
+
123
+ refresh(room) {
124
+ this.rooms.validateRoomId(room);
125
+ this.#queued.delete(room);
126
+ return this.#route.runtime.run(() => this.project(room));
127
+ }
128
+
129
+ async project(room) {
130
+ const generation = Symbol();
131
+ this.#generations.set(room, generation);
132
+ const current = socket => this.#generations.get(room) === generation && this.rooms.has(room, socket) && !this.#joining.has(socket);
133
+ const eligible = [];
134
+ try {
135
+ await Promise.all(this.rooms.members(room).map(async socket => {
136
+ if (!current(socket)) return;
137
+ try {
138
+ await this.check(socket);
139
+ if (current(socket)) { const client = this.get(socket); eligible.push({ client, identity: client.identity }); }
140
+ }
141
+ catch (error) { if (current(socket)) this.exclude(socket, error); }
142
+ }));
143
+ const online = Object.freeze([...new Set(eligible.map(entry => entry.identity))]);
144
+ await Promise.all(eligible.map(async ({ client }) => {
145
+ const socket = client.socket;
146
+ try {
147
+ if (!current(socket)) return;
148
+ const { state, frame } = await this.projection.run(async () => {
149
+ const state = await this.options.project(client, room, online);
150
+ const frame = socket.page ? null : await this.options.raw.update(state);
151
+ return { state, frame };
152
+ }, socket.context.signal);
153
+ await this.check(socket);
154
+ if (!current(socket)) return;
155
+ if (socket.page) Object.assign(client.page, state);
156
+ else socket.sendEvent(frame.type, frame.payload);
157
+ } catch (error) { if (current(socket)) this.exclude(socket, error); }
158
+ }));
159
+ } finally {
160
+ if (this.#generations.get(room) === generation) this.#generations.delete(room);
161
+ }
162
+ }
163
+
164
+ exclude(socket, error) {
165
+ this.rooms.leaveAll(socket);
166
+ if (!(error instanceof AccessDenied)) this.#route.handleError(socket, error);
167
+ socket.close(error instanceof AccessDenied ? 1008 : 1011, 'Connection unavailable.');
168
+ }
169
+
170
+ bind(contract) {
171
+ return Object.freeze({
172
+ protocol: contract.protocol,
173
+ handler: (type, callback) => {
174
+ if (typeof callback !== 'function') throw new TypeError('A client handler requires a callback.');
175
+ return contract.handler(type, async (socket, payload, message) => {
176
+ const client = this.get(socket);
177
+ await this.check(socket);
178
+ try {
179
+ const result = await callback(client, payload);
180
+ if (result === false) return false;
181
+ await Promise.all(client.rooms.map(room => this.refresh(room)));
182
+ if (!socket.page && message.requestId !== undefined) {
183
+ await this.check(socket);
184
+ socket.sendEvent('redweb:result', null, { requestId: message.requestId });
185
+ }
186
+ return result;
187
+ } catch (error) {
188
+ const text = error instanceof ClientError ? error.message : this.options.reject?.(error);
189
+ if (typeof text !== 'string' || !text || text.length > 512) throw error;
190
+ const metadata = { requestId: message.requestId };
191
+ const frame = socket.page || !this.options.raw.reject ? null :
192
+ await this.projection.run(() => this.options.raw.reject(text), socket.context.signal);
193
+ await this.check(socket);
194
+ if (socket.page) {
195
+ if (this.options.errorState) Object.assign(client.page, this.options.errorState(text));
196
+ } else if (frame) socket.sendEvent(frame.type, frame.payload);
197
+ socket.sendProtocolError('COMMAND_REJECTED', text, metadata);
198
+ return false;
199
+ }
200
+ });
201
+ },
202
+ });
203
+ }
204
+ }
205
+
206
+ const connectedClients = options => new ConnectedClients(options);
207
+ module.exports = { connectedClients, ConnectedClients, ConnectedClient, ClientError };
@@ -1,4 +1,4 @@
1
- 'use strict';
2
- // A private hook, never a field supplied by an incoming message.
3
- const guards = new WeakMap();
4
- module.exports = { guards };
1
+ 'use strict';
2
+ // A private hook, never a field supplied by an incoming message.
3
+ const guards = new WeakMap();
4
+ module.exports = { guards };
@@ -102,12 +102,12 @@ class RoomRegistry {
102
102
  return roomIds.length;
103
103
  }
104
104
 
105
- members(roomId) {
105
+ members(roomId) {
106
106
  this.validateRoomId(roomId);
107
107
  return [...(this.rooms.get(roomId) || [])];
108
- }
109
-
110
- roomsFor(socket) { return [...(this.memberships.get(socket) || [])]; }
108
+ }
109
+
110
+ roomsFor(socket) { return [...(this.memberships.get(socket) || [])]; }
111
111
 
112
112
  has(roomId, socket) {
113
113
  this.validateRoomId(roomId);
@@ -5,8 +5,8 @@ const HeartbeatMonitor = require('./HeartbeatMonitor');
5
5
  const RoomRegistry = require('./RoomRegistry');
6
6
  const SessionRegistry = require('./SessionRegistry');
7
7
  const DistributionBridge = require('./DistributionBridge');
8
- const requestSnapshot = require('../context/RequestSnapshot');
9
- const { ConnectedClients } = require('./ConnectedClients');
8
+ const requestSnapshot = require('../context/RequestSnapshot');
9
+ const { ConnectedClients } = require('./ConnectedClients');
10
10
 
11
11
  function joinRoom(roomId) { return this.__redwebRuntimeOwner.rooms.join(roomId, this); }
12
12
  function enterRoom(roomId) { return this.__redwebRuntimeOwner.rooms.enter(roomId, this); }
@@ -24,7 +24,7 @@ function publishEvent(type, payload) { return this.__redwebRuntimeOwner.route.pu
24
24
  function connectionContext() { return this.__redwebRuntimeOwner.ensureContext(this); }
25
25
 
26
26
  class RouteRuntime {
27
- constructor(route, { heartbeat, rooms, sessions, distribution, drainHandlers, connections }) {
27
+ constructor(route, { heartbeat, rooms, sessions, distribution, drainHandlers, connections }) {
28
28
  this.route = route;
29
29
  this.inFlight = drainHandlers ? new Set() : null;
30
30
  this.abortController = drainHandlers ? new AbortController() : null;
@@ -32,8 +32,8 @@ class RouteRuntime {
32
32
  this.contexts = new WeakMap();
33
33
  this.requests = new WeakMap();
34
34
  this.needsContext = Boolean(route.admissionPolicy || route.protocolPolicy || rooms || sessions || drainHandlers);
35
- try {
36
- if (connections !== undefined && !(connections instanceof ConnectedClients)) throw new TypeError('connections must be created with connectedClients().');
35
+ try {
36
+ if (connections !== undefined && !(connections instanceof ConnectedClients)) throw new TypeError('connections must be created with connectedClients().');
37
37
  this.heartbeat = heartbeat === undefined ? null : new HeartbeatMonitor(heartbeat, route.logger);
38
38
  this.rooms = rooms === undefined || rooms === false
39
39
  ? null
@@ -42,10 +42,10 @@ class RouteRuntime {
42
42
  socket.readyState === 1 && this.contexts.get(socket)?.active !== false,
43
43
  contextFor: socket => this.ensureContext(socket),
44
44
  policy: route.transportPolicy,
45
- onChange: (action, roomId) => {
45
+ onChange: (action, roomId) => {
46
46
  route.metrics?.increment(`redweb.room.${action}`);
47
- route.metrics?.gauge('redweb.rooms.active', this.rooms.size);
48
- connections?.changed(roomId);
47
+ route.metrics?.gauge('redweb.rooms.active', this.rooms.size);
48
+ connections?.changed(roomId);
49
49
  },
50
50
  });
51
51
  this.sessions = sessions === undefined || sessions === false
@@ -54,10 +54,10 @@ class RouteRuntime {
54
54
  if (distribution !== undefined && distribution !== false && typeof distribution?.onEvent !== 'function') {
55
55
  throw new TypeError('`distribution.onEvent` must be a function.');
56
56
  }
57
- this.distribution = distribution === undefined || distribution === false
57
+ this.distribution = distribution === undefined || distribution === false
58
58
  ? null
59
- : new DistributionBridge(distribution, event => distribution.onEvent(event, route), route.logger);
60
- connections?.attach(route, this.rooms);
59
+ : new DistributionBridge(distribution, event => distribution.onEvent(event, route), route.logger);
60
+ connections?.attach(route, this.rooms);
61
61
  } catch (error) {
62
62
  this.heartbeat?.stop();
63
63
  this.sessions?.stop();
@@ -1,16 +1,16 @@
1
- 'use strict';
2
-
3
- // Browser-safe metadata keeps the standalone contract entry free of Node rendering dependencies.
4
- const bindings = new WeakMap();
5
- function register(Handler, type) {
6
- bindings.set(Handler, { Handler, type });
7
- Object.defineProperty(Handler, 'with', { value(payload) {
8
- const serialized = JSON.stringify(payload);
9
- if (serialized === undefined || serialized.length > 65536) throw new TypeError('Socket action payload must be bounded JSON.');
10
- const binding = Object.freeze({});
11
- bindings.set(binding, { Handler, type, payload: JSON.parse(serialized) });
12
- return binding;
13
- } });
14
- return Handler;
15
- }
16
- module.exports = { register, bindings };
1
+ 'use strict';
2
+
3
+ // Browser-safe metadata keeps the standalone contract entry free of Node rendering dependencies.
4
+ const bindings = new WeakMap();
5
+ function register(Handler, type) {
6
+ bindings.set(Handler, { Handler, type });
7
+ Object.defineProperty(Handler, 'with', { value(payload) {
8
+ const serialized = JSON.stringify(payload);
9
+ if (serialized === undefined || serialized.length > 65536) throw new TypeError('Socket action payload must be bounded JSON.');
10
+ const binding = Object.freeze({});
11
+ bindings.set(binding, { Handler, type, payload: JSON.parse(serialized) });
12
+ return binding;
13
+ } });
14
+ return Handler;
15
+ }
16
+ module.exports = { register, bindings };
@@ -83,7 +83,7 @@ class SocketContract {
83
83
  if (!this.#validators.has(type)) throw new TypeError('Handler message type is not defined in the contract.');
84
84
  if (typeof callback !== 'function') throw new TypeError('A contract handler requires a callback.');
85
85
  const contract = this;
86
- class ContractHandler extends BaseHandler {
86
+ class ContractHandler extends BaseHandler {
87
87
  constructor() { super(type); }
88
88
  async handleMessage(socket, message) {
89
89
  if (socket.context?.protocol?.version !== contract.version) throw new TypeError('The route must negotiate this contract version before handling messages.');
@@ -93,8 +93,8 @@ class SocketContract {
93
93
  return super.handleMessage(socket, { ...message, payload });
94
94
  }
95
95
  onMessage(socket, message) { return callback(socket, message.payload, message); }
96
- }
97
- return require('./SocketAction').register(ContractHandler, type);
96
+ }
97
+ return require('./SocketAction').register(ContractHandler, type);
98
98
  }
99
99
 
100
100
  client(socket) { return new ContractClient(this, socket); }
@@ -93,12 +93,12 @@ class SocketRoute {
93
93
  limits,
94
94
  orderedMessages = false,
95
95
  heartbeat,
96
- connections,
97
- rooms = connections ? true : undefined,
96
+ connections,
97
+ rooms = connections ? true : undefined,
98
98
  sessions,
99
99
  metrics,
100
100
  distribution,
101
- drainHandlers = Boolean(connections),
101
+ drainHandlers = Boolean(connections),
102
102
  protocol,
103
103
  maxPendingUpgrades = 64,
104
104
  } = {}) {
@@ -193,7 +193,7 @@ class SocketRoute {
193
193
  throw error;
194
194
  }
195
195
  try {
196
- this.runtime = new RouteRuntime(this, { heartbeat, rooms, sessions, distribution, drainHandlers, connections });
196
+ this.runtime = new RouteRuntime(this, { heartbeat, rooms, sessions, distribution, drainHandlers, connections });
197
197
  Object.assign(this, this.runtime.expose());
198
198
  } catch (error) {
199
199
  this.disposeServices();
@@ -453,9 +453,9 @@ class SocketRoute {
453
453
  return false;
454
454
  } else {
455
455
  try {
456
- // A handler may send a recoverable protocol error and explicitly decline
457
- // success. Undefined remains successful for existing command handlers.
458
- return await handler.handleMessage(sock, data) !== false;
456
+ // A handler may send a recoverable protocol error and explicitly decline
457
+ // success. Undefined remains successful for existing command handlers.
458
+ return await handler.handleMessage(sock, data) !== false;
459
459
  } catch (error) {
460
460
  if (this.sendAccessFailure(sock, error, { requestId: data.requestId })) return false;
461
461
  if (error instanceof InboundContractValidationError) {
@@ -0,0 +1,16 @@
1
+ .redweb-code {
2
+ color: #e5e7eb;
3
+ background: #111827;
4
+ border-radius: 0.75rem;
5
+ overflow: hidden;
6
+ }
7
+ .redweb-code pre {
8
+ overflow-x: auto;
9
+ padding: 1rem;
10
+ }
11
+ .redweb-code .token-comment { color: #7dd3fc; }
12
+ .redweb-code .token-keyword { color: #93c5fd; }
13
+ .redweb-code .token-literal { color: #c4b5fd; }
14
+ .redweb-code .token-number { color: #fcd34d; }
15
+ .redweb-code .token-string { color: #fca5a5; }
16
+ .redweb-code .token-reference { color: #a7f3d0; }