redweb 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -1
- package/README.md +3 -3
- package/docs/CONNECTED_CLIENTS_VERIFICATION.md +65 -0
- package/docs/SOCKET_PAGES.md +73 -0
- package/docs/generated.json +217 -217
- package/docs/releases/0.16.0.json +2217 -0
- package/index.d.ts +51 -4
- package/index.js +7 -2
- package/package.json +1 -1
- package/src/ws/ConnectedClients.js +207 -0
- package/src/ws/RoomRegistry.js +4 -2
- package/src/ws/RouteRuntime.js +11 -7
- package/src/ws/SocketRoute.js +4 -3
package/index.d.ts
CHANGED
|
@@ -51,13 +51,57 @@ declare module 'redweb' {
|
|
|
51
51
|
sendBinaryEvent?(value: unknown): Promise<boolean>;
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
export interface RedWebConnectionContext extends RequestContext {
|
|
54
|
+
export interface RedWebConnectionContext extends RequestContext {
|
|
55
55
|
readonly connectionId: string;
|
|
56
56
|
readonly principal: unknown;
|
|
57
57
|
session: unknown | null;
|
|
58
58
|
metadata: Record<string, unknown>;
|
|
59
59
|
readonly protocol?: Readonly<{ version: string }>;
|
|
60
|
-
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A deliberate, safe command rejection, unlike an unexpected application error. */
|
|
63
|
+
export class ClientError extends Error { constructor(message: string); }
|
|
64
|
+
export interface ClientEvent { type: string; payload: unknown; }
|
|
65
|
+
export interface ConnectedClientsOptions<Page extends object, Identity> {
|
|
66
|
+
/** Must still equal the admitted principal. Returning another identity never transfers a connection. */
|
|
67
|
+
identity(context: RedWebConnectionContext): Identity | undefined | false | Promise<Identity | undefined | false>;
|
|
68
|
+
page(): new () => Page;
|
|
69
|
+
project(client: ConnectedClient<Page, Identity>, room: string, online: readonly Identity[]): Partial<Page> | Promise<Partial<Page>>;
|
|
70
|
+
authorizationTimeoutMs?: number;
|
|
71
|
+
projectionTimeoutMs?: number;
|
|
72
|
+
/** Explicitly classify domain errors safe to display; all other failures remain private. */
|
|
73
|
+
reject?(error: unknown): string | undefined;
|
|
74
|
+
errorState?(message: string): Partial<Page>;
|
|
75
|
+
/** Optional non-HTML client support. Redweb performs the final checked send. */
|
|
76
|
+
raw?: {
|
|
77
|
+
update(state: Partial<Page>): ClientEvent | Promise<ClientEvent>;
|
|
78
|
+
reject?(message: string): ClientEvent | Promise<ClientEvent>;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export class ConnectedClient<Page extends object, Identity = unknown> {
|
|
82
|
+
private constructor();
|
|
83
|
+
readonly socket: RedWebSocket;
|
|
84
|
+
readonly identity: Identity;
|
|
85
|
+
readonly page: Page;
|
|
86
|
+
readonly rooms: readonly string[];
|
|
87
|
+
/** Requires exactly one current room; use rooms for multi-room applications. */
|
|
88
|
+
readonly room: string;
|
|
89
|
+
/** Reserve room capacity, run a synchronous domain commit, and roll back new membership if it rejects. */
|
|
90
|
+
join(room: string, commit?: () => unknown): Promise<void>;
|
|
91
|
+
leave(room: string): boolean;
|
|
92
|
+
}
|
|
93
|
+
export class ConnectedClients<Page extends object, Identity = unknown> {
|
|
94
|
+
constructor(options: ConnectedClientsOptions<Page, Identity>);
|
|
95
|
+
get(socket: RedWebSocket): ConnectedClient<Page, Identity>;
|
|
96
|
+
refresh(room: string): Promise<unknown>;
|
|
97
|
+
bind<Schemas extends import('redweb/contract').SocketSchemas>(contract: import('redweb/contract').SocketContract<Schemas>): {
|
|
98
|
+
readonly protocol: { readonly versions: readonly string[] };
|
|
99
|
+
handler<Type extends keyof Schemas & string>(type: Type, callback: (
|
|
100
|
+
client: ConnectedClient<Page, Identity>, payload: import('redweb/contract').ContractOutput<Schemas[Type]>,
|
|
101
|
+
) => unknown): import('redweb/contract').SocketHandler<import('redweb/contract').ContractInput<Schemas[Type]>>;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export function connectedClients<Page extends object, Identity>(options: ConnectedClientsOptions<Page, Identity>): ConnectedClients<Page, Identity>;
|
|
61
105
|
|
|
62
106
|
export interface AdmissionContext {
|
|
63
107
|
signal: AbortSignal;
|
|
@@ -284,7 +328,9 @@ declare module 'redweb' {
|
|
|
284
328
|
|
|
285
329
|
/** ─────────────────── ROUTES & HANDLERS ─────────────────── */
|
|
286
330
|
|
|
287
|
-
export interface SocketRouteConfig {
|
|
331
|
+
export interface SocketRouteConfig {
|
|
332
|
+
/** Opt-in server-side clients, with RoomRegistry membership and private page projection. */
|
|
333
|
+
connections?: ConnectedClients<any, any>;
|
|
288
334
|
path: string;
|
|
289
335
|
handlers: Array<new () => BaseHandler>;
|
|
290
336
|
services?: Array<new () => SocketService>;
|
|
@@ -414,7 +460,8 @@ declare module 'redweb' {
|
|
|
414
460
|
enter(roomId: string, socket: RedWebSocket): Promise<boolean>;
|
|
415
461
|
leave(roomId: string, socket: RedWebSocket): boolean;
|
|
416
462
|
leaveAll(socket: RedWebSocket): number;
|
|
417
|
-
members(roomId: string): RedWebSocket[];
|
|
463
|
+
members(roomId: string): RedWebSocket[];
|
|
464
|
+
roomsFor(socket: RedWebSocket): string[];
|
|
418
465
|
has(roomId: string, socket: RedWebSocket): boolean;
|
|
419
466
|
broadcast(roomId: string, data: unknown, options?: { except?: RedWebSocket }): number;
|
|
420
467
|
broadcastFrom(socket: RedWebSocket, roomId: string, data: unknown, options?: { except?: RedWebSocket }): number;
|
package/index.js
CHANGED
|
@@ -15,13 +15,18 @@ const {
|
|
|
15
15
|
} = require('./src/ws');
|
|
16
16
|
const { BaseHandler } = require('./src/ws/BaseHandler');
|
|
17
17
|
const { defineSocketContract } = require('./contract');
|
|
18
|
-
const { Application, defineApp } = require('./src/Application');
|
|
18
|
+
const { Application, defineApp } = require('./src/Application');
|
|
19
|
+
const { connectedClients, ConnectedClients, ConnectedClient, ClientError } = require('./src/ws/ConnectedClients');
|
|
19
20
|
const HttpServer = require('./src/http/HttpServer');
|
|
20
21
|
const HttpsServer = require('./src/http/HttpsServer');
|
|
21
22
|
const { action, attribute, codeBlock, component, defineSite, each, exportStatic, html, HtmlRenderer, LiveHtmlServer, LivePage, page, start, state, url, view } = require('./src/htmx');
|
|
22
23
|
module.exports = {
|
|
23
24
|
Application,
|
|
24
|
-
defineApp,
|
|
25
|
+
defineApp,
|
|
26
|
+
connectedClients,
|
|
27
|
+
ConnectedClients,
|
|
28
|
+
ConnectedClient,
|
|
29
|
+
ClientError,
|
|
25
30
|
HttpServer,
|
|
26
31
|
HttpsServer,
|
|
27
32
|
BaseHttpServer,
|
package/package.json
CHANGED
|
@@ -0,0 +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 };
|
package/src/ws/RoomRegistry.js
CHANGED
|
@@ -102,10 +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
|
-
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
roomsFor(socket) { return [...(this.memberships.get(socket) || [])]; }
|
|
109
111
|
|
|
110
112
|
has(roomId, socket) {
|
|
111
113
|
this.validateRoomId(roomId);
|
package/src/ws/RouteRuntime.js
CHANGED
|
@@ -5,7 +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');
|
|
8
|
+
const requestSnapshot = require('../context/RequestSnapshot');
|
|
9
|
+
const { ConnectedClients } = require('./ConnectedClients');
|
|
9
10
|
|
|
10
11
|
function joinRoom(roomId) { return this.__redwebRuntimeOwner.rooms.join(roomId, this); }
|
|
11
12
|
function enterRoom(roomId) { return this.__redwebRuntimeOwner.rooms.enter(roomId, this); }
|
|
@@ -23,7 +24,7 @@ function publishEvent(type, payload) { return this.__redwebRuntimeOwner.route.pu
|
|
|
23
24
|
function connectionContext() { return this.__redwebRuntimeOwner.ensureContext(this); }
|
|
24
25
|
|
|
25
26
|
class RouteRuntime {
|
|
26
|
-
constructor(route, { heartbeat, rooms, sessions, distribution, drainHandlers }) {
|
|
27
|
+
constructor(route, { heartbeat, rooms, sessions, distribution, drainHandlers, connections }) {
|
|
27
28
|
this.route = route;
|
|
28
29
|
this.inFlight = drainHandlers ? new Set() : null;
|
|
29
30
|
this.abortController = drainHandlers ? new AbortController() : null;
|
|
@@ -31,7 +32,8 @@ class RouteRuntime {
|
|
|
31
32
|
this.contexts = new WeakMap();
|
|
32
33
|
this.requests = new WeakMap();
|
|
33
34
|
this.needsContext = Boolean(route.admissionPolicy || route.protocolPolicy || rooms || sessions || drainHandlers);
|
|
34
|
-
try {
|
|
35
|
+
try {
|
|
36
|
+
if (connections !== undefined && !(connections instanceof ConnectedClients)) throw new TypeError('connections must be created with connectedClients().');
|
|
35
37
|
this.heartbeat = heartbeat === undefined ? null : new HeartbeatMonitor(heartbeat, route.logger);
|
|
36
38
|
this.rooms = rooms === undefined || rooms === false
|
|
37
39
|
? null
|
|
@@ -40,9 +42,10 @@ class RouteRuntime {
|
|
|
40
42
|
socket.readyState === 1 && this.contexts.get(socket)?.active !== false,
|
|
41
43
|
contextFor: socket => this.ensureContext(socket),
|
|
42
44
|
policy: route.transportPolicy,
|
|
43
|
-
onChange: action => {
|
|
45
|
+
onChange: (action, roomId) => {
|
|
44
46
|
route.metrics?.increment(`redweb.room.${action}`);
|
|
45
|
-
route.metrics?.gauge('redweb.rooms.active', this.rooms.size);
|
|
47
|
+
route.metrics?.gauge('redweb.rooms.active', this.rooms.size);
|
|
48
|
+
connections?.changed(roomId);
|
|
46
49
|
},
|
|
47
50
|
});
|
|
48
51
|
this.sessions = sessions === undefined || sessions === false
|
|
@@ -51,9 +54,10 @@ class RouteRuntime {
|
|
|
51
54
|
if (distribution !== undefined && distribution !== false && typeof distribution?.onEvent !== 'function') {
|
|
52
55
|
throw new TypeError('`distribution.onEvent` must be a function.');
|
|
53
56
|
}
|
|
54
|
-
this.distribution = distribution === undefined || distribution === false
|
|
57
|
+
this.distribution = distribution === undefined || distribution === false
|
|
55
58
|
? null
|
|
56
|
-
: new DistributionBridge(distribution, event => distribution.onEvent(event, route), route.logger);
|
|
59
|
+
: new DistributionBridge(distribution, event => distribution.onEvent(event, route), route.logger);
|
|
60
|
+
connections?.attach(route, this.rooms);
|
|
57
61
|
} catch (error) {
|
|
58
62
|
this.heartbeat?.stop();
|
|
59
63
|
this.sessions?.stop();
|
package/src/ws/SocketRoute.js
CHANGED
|
@@ -93,11 +93,12 @@ class SocketRoute {
|
|
|
93
93
|
limits,
|
|
94
94
|
orderedMessages = false,
|
|
95
95
|
heartbeat,
|
|
96
|
-
|
|
96
|
+
connections,
|
|
97
|
+
rooms = connections ? true : undefined,
|
|
97
98
|
sessions,
|
|
98
99
|
metrics,
|
|
99
100
|
distribution,
|
|
100
|
-
drainHandlers =
|
|
101
|
+
drainHandlers = Boolean(connections),
|
|
101
102
|
protocol,
|
|
102
103
|
maxPendingUpgrades = 64,
|
|
103
104
|
} = {}) {
|
|
@@ -192,7 +193,7 @@ class SocketRoute {
|
|
|
192
193
|
throw error;
|
|
193
194
|
}
|
|
194
195
|
try {
|
|
195
|
-
this.runtime = new RouteRuntime(this, { heartbeat, rooms, sessions, distribution, drainHandlers });
|
|
196
|
+
this.runtime = new RouteRuntime(this, { heartbeat, rooms, sessions, distribution, drainHandlers, connections });
|
|
196
197
|
Object.assign(this, this.runtime.expose());
|
|
197
198
|
} catch (error) {
|
|
198
199
|
this.disposeServices();
|