@rlanz/socket 0.0.1-3 → 0.0.1-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.
- package/README.md +48 -27
- package/build/{chunk-NRWRLZAO.js → chunk-ILDK672E.js} +180 -53
- package/build/chunk-ILDK672E.js.map +1 -0
- package/build/index.d.ts +16 -2
- package/build/index.js +20 -2
- package/build/index.js.map +1 -1
- package/build/providers/socket_provider.d.ts +2 -2
- package/build/providers/socket_provider.js +1 -1
- package/build/services/socket.d.ts +2 -2
- package/build/{socket_service-BN1g3ENk.d.ts → socket_service-CRmPa74K.d.ts} +2 -2
- package/build/src/client/index.js +0 -5
- package/build/src/client/index.js.map +1 -1
- package/build/src/client/types.d.ts +0 -5
- package/build/src/decorators.d.ts +1 -1
- package/build/src/health_check.d.ts +3 -3
- package/build/src/types.d.ts +1 -1
- package/build/{types-BnOi46bE.d.ts → types-BBLNfcWk.d.ts} +35 -15
- package/package.json +3 -3
- package/build/chunk-NRWRLZAO.js.map +0 -1
package/README.md
CHANGED
|
@@ -23,26 +23,39 @@ Create a socket config file and tune the WebSocket path or heartbeat.
|
|
|
23
23
|
|
|
24
24
|
```ts
|
|
25
25
|
// config/socket.ts
|
|
26
|
-
import { defineConfig } from '@
|
|
26
|
+
import { defineConfig } from '@rlanz/socket'
|
|
27
27
|
|
|
28
28
|
export default defineConfig({
|
|
29
29
|
websocket: {
|
|
30
30
|
path: '/socket',
|
|
31
|
-
pingInterval:
|
|
32
|
-
pingTimeout:
|
|
31
|
+
pingInterval: '25s',
|
|
32
|
+
pingTimeout: '5s',
|
|
33
|
+
maxBufferedAmount: 16 * 1024 * 1024,
|
|
33
34
|
},
|
|
34
35
|
})
|
|
35
36
|
```
|
|
36
37
|
|
|
37
38
|
- `websocket.path` defaults to `/socket`.
|
|
38
|
-
- `websocket.
|
|
39
|
+
- `websocket.middleware` runs AdonisJS HTTP middleware for the WebSocket upgrade request.
|
|
40
|
+
- `websocket.pingInterval` sends a WebSocket ping every configured duration.
|
|
39
41
|
- `websocket.pingTimeout` closes connections that do not answer in time.
|
|
40
42
|
- If only one heartbeat value is configured, the other one uses the default shown above.
|
|
43
|
+
- `websocket.maxPayload` limits inbound message payloads; it defaults to 1 MiB.
|
|
44
|
+
- `websocket.maxQueuedMessages` limits unresolved non-ping protocol messages per socket; it defaults
|
|
45
|
+
to `100`.
|
|
46
|
+
- `websocket.maxMessagesPerInterval` limits inbound protocol messages per
|
|
47
|
+
`websocket.messageRateInterval`; they default to `1000` messages per `1s`.
|
|
48
|
+
- `websocket.maxBufferedAmount` closes slow outbound sockets whose WebSocket buffer is already above
|
|
49
|
+
the configured byte limit before an event is sent; it defaults to 16 MiB.
|
|
50
|
+
|
|
51
|
+
`websocket.middleware` is different from channel middleware. Use it to prepare request-scoped
|
|
52
|
+
services such as sessions or auth during the initial HTTP upgrade; use `static middlewares` on a
|
|
53
|
+
channel class to authorize or enrich individual channel subscriptions.
|
|
41
54
|
|
|
42
55
|
### Authentication
|
|
43
56
|
|
|
44
|
-
Authenticate sockets during the HTTP upgrade with `websocket.authenticate`. The hook receives
|
|
45
|
-
|
|
57
|
+
Authenticate sockets during the HTTP upgrade with `websocket.authenticate`. The hook receives an
|
|
58
|
+
AdonisJS HTTP context and returns the authenticated user.
|
|
46
59
|
|
|
47
60
|
Define `websocket.middleware` when your WebSocket upgrade authentication depends on AdonisJS
|
|
48
61
|
middleware such as sessions or auth initialization. These middleware run only for the HTTP upgrade
|
|
@@ -54,34 +67,34 @@ channel itself through `static middlewares`.
|
|
|
54
67
|
|
|
55
68
|
```ts
|
|
56
69
|
// config/socket.ts
|
|
57
|
-
import {
|
|
70
|
+
import type { HttpContext } from '@adonisjs/core/http'
|
|
71
|
+
import { authenticateWithAdonisAuth, defineConfig } from '@rlanz/socket'
|
|
58
72
|
|
|
59
|
-
|
|
73
|
+
type User = { id: string | number; name: string }
|
|
74
|
+
|
|
75
|
+
export default defineConfig<User, HttpContext>({
|
|
60
76
|
websocket: {
|
|
61
77
|
middleware: [
|
|
62
78
|
() => import('@adonisjs/session/session_middleware'),
|
|
63
79
|
() => import('@adonisjs/auth/initialize_auth_middleware'),
|
|
64
80
|
],
|
|
65
81
|
|
|
66
|
-
|
|
67
|
-
await httpContext!.auth.authenticateUsing()
|
|
68
|
-
const user = httpContext!.auth.getUserOrFail()
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
user,
|
|
72
|
-
auth,
|
|
73
|
-
data: {
|
|
74
|
-
connectedAt: new Date(),
|
|
75
|
-
},
|
|
76
|
-
}
|
|
77
|
-
},
|
|
82
|
+
authenticate: authenticateWithAdonisAuth<User, HttpContext>(),
|
|
78
83
|
},
|
|
79
84
|
})
|
|
80
85
|
```
|
|
81
86
|
|
|
82
|
-
Return `false`, `null`, or throw to reject the upgrade with `401 Unauthorized`. The returned
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
Return `false`, `null`, or throw to reject the upgrade with `401 Unauthorized`. The returned user is
|
|
88
|
+
available as `socket.user`, and the HTTP context remains available as `socket.raw.httpContext`.
|
|
89
|
+
|
|
90
|
+
Override `websocket.authenticate` when you need custom behavior:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
async authenticate({ httpContext }) {
|
|
94
|
+
await httpContext!.auth.authenticateUsing()
|
|
95
|
+
return httpContext!.auth.getUserOrFail()
|
|
96
|
+
}
|
|
97
|
+
```
|
|
85
98
|
|
|
86
99
|
### Horizontal Sync
|
|
87
100
|
|
|
@@ -89,8 +102,8 @@ Configure an [`@boringnode/bus`](https://github.com/boringnode/bus) transport to
|
|
|
89
102
|
|
|
90
103
|
```ts
|
|
91
104
|
// config/socket.ts
|
|
92
|
-
import { defineConfig } from '@adonisjs/core/config'
|
|
93
105
|
import { redis } from '@boringnode/bus/transports/redis'
|
|
106
|
+
import { defineConfig } from '@rlanz/socket'
|
|
94
107
|
|
|
95
108
|
export default defineConfig({
|
|
96
109
|
transport: {
|
|
@@ -99,14 +112,14 @@ export default defineConfig({
|
|
|
99
112
|
port: 6379,
|
|
100
113
|
}),
|
|
101
114
|
channel: 'socket::broadcast',
|
|
102
|
-
presenceTimeout:
|
|
115
|
+
presenceTimeout: '100ms',
|
|
103
116
|
},
|
|
104
117
|
})
|
|
105
118
|
```
|
|
106
119
|
|
|
107
120
|
When configured, `socket.to(channel).emit(...)`, `socket.to(channel).except(socketId).emit(...)`, `socket.toUser(userId).emit(...)`, and `socket.broadcast(...)` are delivered locally and published to the bus so other instances can deliver them to their own connected sockets.
|
|
108
121
|
|
|
109
|
-
The default bus channel is `socket::broadcast`. Presence channels also use the bus to build distributed snapshots during subscribe, duplicate subscribe, join, and leave updates. `presenceTimeout` controls how long an instance waits for other instances to answer a presence snapshot request; it defaults to `
|
|
122
|
+
The default bus channel is `socket::broadcast`. Presence channels also use the bus to build distributed snapshots during subscribe, duplicate subscribe, join, and leave updates. `presenceTimeout` controls how long an instance waits for other instances to answer a presence snapshot request; it defaults to `100ms`.
|
|
110
123
|
|
|
111
124
|
### Health Checks
|
|
112
125
|
|
|
@@ -337,7 +350,6 @@ import { Socket } from '@rlanz/socket/client'
|
|
|
337
350
|
const socket = new Socket({
|
|
338
351
|
url: 'http://localhost:3333',
|
|
339
352
|
path: '/socket',
|
|
340
|
-
auth: { token: 'secret' },
|
|
341
353
|
autoReconnect: true,
|
|
342
354
|
reconnectDelay: 250,
|
|
343
355
|
reconnectMaxDelay: 5000,
|
|
@@ -399,7 +411,11 @@ Set `autoReconnect: false` to disable reconnect attempts.
|
|
|
399
411
|
import { BaseChannel, ChannelRouter, PresenceManager, SocketService } from '@rlanz/socket'
|
|
400
412
|
import type { AuthenticatedSocket, SocketConfig } from '@rlanz/socket/types'
|
|
401
413
|
import SocketProvider from '@rlanz/socket/provider'
|
|
414
|
+
import socket from '@rlanz/socket/services/main'
|
|
402
415
|
import { onMessage } from '@rlanz/socket/decorators'
|
|
416
|
+
import { SocketHealthCheck } from '@rlanz/socket/health_check'
|
|
417
|
+
import { SocketInstrumentation } from '@rlanz/socket/otel'
|
|
418
|
+
import { SocketFake } from '@rlanz/socket/testing'
|
|
403
419
|
import { Socket, Channel } from '@rlanz/socket/client'
|
|
404
420
|
import type { SocketOptions, PresenceData } from '@rlanz/socket/client/types'
|
|
405
421
|
```
|
|
@@ -408,7 +424,12 @@ Available exports:
|
|
|
408
424
|
|
|
409
425
|
- `@rlanz/socket`
|
|
410
426
|
- `@rlanz/socket/provider`
|
|
427
|
+
- `@rlanz/socket/services/main`
|
|
411
428
|
- `@rlanz/socket/decorators`
|
|
429
|
+
- `@rlanz/socket/health_check`
|
|
430
|
+
- `@rlanz/socket/otel`
|
|
431
|
+
- `@rlanz/socket/testing`
|
|
412
432
|
- `@rlanz/socket/types`
|
|
433
|
+
- `@rlanz/socket/types/tracing_channels`
|
|
413
434
|
- `@rlanz/socket/client`
|
|
414
435
|
- `@rlanz/socket/client/types`
|
|
@@ -797,22 +797,37 @@ function isClientMessageType(value) {
|
|
|
797
797
|
// src/message_queue.ts
|
|
798
798
|
var MessageQueue = class {
|
|
799
799
|
#queues = /* @__PURE__ */ new Map();
|
|
800
|
-
|
|
800
|
+
#depths = /* @__PURE__ */ new Map();
|
|
801
|
+
enqueue(key, task, options = {}) {
|
|
802
|
+
const depth = this.#depths.get(key) ?? 0;
|
|
803
|
+
if (options.maxDepth !== void 0 && depth >= options.maxDepth) {
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
this.#depths.set(key, depth + 1);
|
|
801
807
|
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
802
808
|
const next = previous.catch(() => {
|
|
803
809
|
}).then(task).catch(() => {
|
|
804
810
|
}).finally(() => {
|
|
811
|
+
const nextDepth = (this.#depths.get(key) ?? 1) - 1;
|
|
812
|
+
if (nextDepth <= 0) {
|
|
813
|
+
this.#depths.delete(key);
|
|
814
|
+
} else {
|
|
815
|
+
this.#depths.set(key, nextDepth);
|
|
816
|
+
}
|
|
805
817
|
if (this.#queues.get(key) === next) {
|
|
806
818
|
this.#queues.delete(key);
|
|
807
819
|
}
|
|
808
820
|
});
|
|
809
821
|
this.#queues.set(key, next);
|
|
822
|
+
return true;
|
|
810
823
|
}
|
|
811
824
|
delete(key) {
|
|
812
825
|
this.#queues.delete(key);
|
|
826
|
+
this.#depths.delete(key);
|
|
813
827
|
}
|
|
814
828
|
clear() {
|
|
815
829
|
this.#queues.clear();
|
|
830
|
+
this.#depths.clear();
|
|
816
831
|
}
|
|
817
832
|
};
|
|
818
833
|
|
|
@@ -824,13 +839,28 @@ function userRoom(userId) {
|
|
|
824
839
|
// src/socket_bus.ts
|
|
825
840
|
import { randomUUID } from "crypto";
|
|
826
841
|
import { Bus } from "@boringnode/bus";
|
|
842
|
+
|
|
843
|
+
// src/duration.ts
|
|
844
|
+
import { parse as parseDurationExpression } from "@lukeed/ms";
|
|
845
|
+
function parseDuration(name, duration) {
|
|
846
|
+
if (duration === void 0) {
|
|
847
|
+
return void 0;
|
|
848
|
+
}
|
|
849
|
+
const milliseconds = typeof duration === "number" ? duration : parseDurationExpression(duration);
|
|
850
|
+
if (typeof milliseconds === "undefined" || !Number.isFinite(milliseconds) || milliseconds <= 0) {
|
|
851
|
+
throw new Error(`${name} must be a positive duration`);
|
|
852
|
+
}
|
|
853
|
+
return milliseconds;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// src/socket_bus.ts
|
|
827
857
|
var DEFAULT_CHANNEL = "socket::broadcast";
|
|
828
858
|
var DEFAULT_PRESENCE_TIMEOUT = 100;
|
|
829
859
|
var SocketBus = class {
|
|
830
860
|
constructor(transport, handlers) {
|
|
831
861
|
this.handlers = handlers;
|
|
832
862
|
this.#channel = transport.channel ?? DEFAULT_CHANNEL;
|
|
833
|
-
this.#presenceTimeout = transport.presenceTimeout ?? DEFAULT_PRESENCE_TIMEOUT;
|
|
863
|
+
this.#presenceTimeout = parseDuration("transport.presenceTimeout", transport.presenceTimeout) ?? DEFAULT_PRESENCE_TIMEOUT;
|
|
834
864
|
this.#bus = new Bus(transport.driver(), { retryQueue: { enabled: true } });
|
|
835
865
|
}
|
|
836
866
|
#origin = randomUUID();
|
|
@@ -999,23 +1029,20 @@ var SocketUpgrader = class _SocketUpgrader {
|
|
|
999
1029
|
return url.pathname === this.#path;
|
|
1000
1030
|
}
|
|
1001
1031
|
async #authenticate(request) {
|
|
1002
|
-
const auth = this.#parseAuth(request);
|
|
1003
1032
|
const config = this.config;
|
|
1004
1033
|
if (!config) {
|
|
1005
|
-
return {
|
|
1034
|
+
return {};
|
|
1006
1035
|
}
|
|
1007
1036
|
if (!config.authenticate && !config.createContextOnUpgrade) {
|
|
1008
|
-
return {
|
|
1037
|
+
return {};
|
|
1009
1038
|
}
|
|
1010
1039
|
let httpContext;
|
|
1011
1040
|
try {
|
|
1012
1041
|
httpContext = await config.createContext?.(request);
|
|
1013
1042
|
if (!config.authenticate) {
|
|
1014
|
-
return {
|
|
1043
|
+
return { httpContext };
|
|
1015
1044
|
}
|
|
1016
1045
|
const ctx = {
|
|
1017
|
-
request,
|
|
1018
|
-
auth,
|
|
1019
1046
|
httpContext
|
|
1020
1047
|
};
|
|
1021
1048
|
const result = await config.authenticate(ctx);
|
|
@@ -1023,51 +1050,62 @@ var SocketUpgrader = class _SocketUpgrader {
|
|
|
1023
1050
|
return null;
|
|
1024
1051
|
}
|
|
1025
1052
|
return {
|
|
1026
|
-
auth: result.auth ?? auth,
|
|
1027
|
-
data: result.data,
|
|
1028
1053
|
httpContext,
|
|
1029
|
-
user: result
|
|
1054
|
+
user: result
|
|
1030
1055
|
};
|
|
1031
1056
|
} catch {
|
|
1032
1057
|
return null;
|
|
1033
1058
|
}
|
|
1034
1059
|
}
|
|
1035
|
-
#parseAuth(request) {
|
|
1036
|
-
const url = new URL(request.url ?? "/", "ws://localhost");
|
|
1037
|
-
const auth = url.searchParams.get("auth");
|
|
1038
|
-
if (!auth) {
|
|
1039
|
-
return void 0;
|
|
1040
|
-
}
|
|
1041
|
-
try {
|
|
1042
|
-
const parsed = JSON.parse(auth);
|
|
1043
|
-
return parsed && typeof parsed === "object" ? parsed : void 0;
|
|
1044
|
-
} catch {
|
|
1045
|
-
return void 0;
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
1060
|
};
|
|
1049
1061
|
|
|
1050
1062
|
// src/socket_service.ts
|
|
1051
1063
|
var DEFAULT_PING_INTERVAL = 25e3;
|
|
1052
1064
|
var DEFAULT_PING_TIMEOUT = 5e3;
|
|
1065
|
+
var DEFAULT_MAX_PAYLOAD = 1024 * 1024;
|
|
1066
|
+
var DEFAULT_MAX_QUEUED_MESSAGES = 100;
|
|
1067
|
+
var DEFAULT_MAX_MESSAGES_PER_INTERVAL = 1e3;
|
|
1068
|
+
var DEFAULT_MESSAGE_RATE_INTERVAL = 1e3;
|
|
1069
|
+
var DEFAULT_MAX_BUFFERED_AMOUNT = 16 * 1024 * 1024;
|
|
1070
|
+
var CLOSE_POLICY_VIOLATION = 1008;
|
|
1071
|
+
var CLOSE_MESSAGE_TOO_BIG = 1009;
|
|
1072
|
+
var CLOSE_OUTBOUND_BUFFER_LIMIT = "Socket outbound buffer limit exceeded";
|
|
1073
|
+
function serializeFrame(frame) {
|
|
1074
|
+
return JSON.stringify(frame);
|
|
1075
|
+
}
|
|
1076
|
+
function sendSerializedFrame(connection, serializedFrame) {
|
|
1077
|
+
if (connection.readyState !== WebSocket.OPEN) {
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
connection.send(serializedFrame);
|
|
1081
|
+
return true;
|
|
1082
|
+
}
|
|
1053
1083
|
function sendFrame(connection, frame) {
|
|
1084
|
+
return sendSerializedFrame(connection, serializeFrame(frame));
|
|
1085
|
+
}
|
|
1086
|
+
function sendSerializedFrameWithBackpressure(connection, serializedFrame, config) {
|
|
1054
1087
|
if (connection.readyState !== WebSocket.OPEN) {
|
|
1055
|
-
return;
|
|
1088
|
+
return false;
|
|
1056
1089
|
}
|
|
1057
|
-
connection.
|
|
1090
|
+
if (connection.bufferedAmount > config.maxBufferedAmount) {
|
|
1091
|
+
closeConnection(connection, CLOSE_POLICY_VIOLATION, CLOSE_OUTBOUND_BUFFER_LIMIT);
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
connection.send(serializedFrame);
|
|
1095
|
+
return true;
|
|
1058
1096
|
}
|
|
1059
|
-
function
|
|
1097
|
+
function parsePositiveInteger(name, value, fallback) {
|
|
1060
1098
|
if (value === void 0) {
|
|
1061
|
-
return
|
|
1099
|
+
return fallback;
|
|
1062
1100
|
}
|
|
1063
|
-
if (!Number.
|
|
1064
|
-
throw new Error(
|
|
1101
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
1102
|
+
throw new Error(`${name} must be a positive integer`);
|
|
1065
1103
|
}
|
|
1066
1104
|
return value;
|
|
1067
1105
|
}
|
|
1068
1106
|
function resolveHeartbeatConfig(config) {
|
|
1069
|
-
const pingInterval =
|
|
1070
|
-
const pingTimeout =
|
|
1107
|
+
const pingInterval = parseDuration("websocket.pingInterval", config?.pingInterval);
|
|
1108
|
+
const pingTimeout = parseDuration("websocket.pingTimeout", config?.pingTimeout);
|
|
1071
1109
|
if (pingInterval === void 0 && pingTimeout === void 0) {
|
|
1072
1110
|
return null;
|
|
1073
1111
|
}
|
|
@@ -1076,6 +1114,47 @@ function resolveHeartbeatConfig(config) {
|
|
|
1076
1114
|
timeout: pingTimeout ?? DEFAULT_PING_TIMEOUT
|
|
1077
1115
|
};
|
|
1078
1116
|
}
|
|
1117
|
+
function resolveInboundMessageConfig(config) {
|
|
1118
|
+
const messageRateInterval = parseDuration("websocket.messageRateInterval", config?.messageRateInterval) ?? DEFAULT_MESSAGE_RATE_INTERVAL;
|
|
1119
|
+
return {
|
|
1120
|
+
maxPayload: parsePositiveInteger(
|
|
1121
|
+
"websocket.maxPayload",
|
|
1122
|
+
config?.maxPayload,
|
|
1123
|
+
DEFAULT_MAX_PAYLOAD
|
|
1124
|
+
),
|
|
1125
|
+
maxQueuedMessages: parsePositiveInteger(
|
|
1126
|
+
"websocket.maxQueuedMessages",
|
|
1127
|
+
config?.maxQueuedMessages,
|
|
1128
|
+
DEFAULT_MAX_QUEUED_MESSAGES
|
|
1129
|
+
),
|
|
1130
|
+
maxMessagesPerInterval: parsePositiveInteger(
|
|
1131
|
+
"websocket.maxMessagesPerInterval",
|
|
1132
|
+
config?.maxMessagesPerInterval,
|
|
1133
|
+
DEFAULT_MAX_MESSAGES_PER_INTERVAL
|
|
1134
|
+
),
|
|
1135
|
+
messageRateInterval
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
function resolveOutboundMessageConfig(config) {
|
|
1139
|
+
return {
|
|
1140
|
+
maxBufferedAmount: parsePositiveInteger(
|
|
1141
|
+
"websocket.maxBufferedAmount",
|
|
1142
|
+
config?.maxBufferedAmount,
|
|
1143
|
+
DEFAULT_MAX_BUFFERED_AMOUNT
|
|
1144
|
+
)
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
function getRawDataByteLength(data) {
|
|
1148
|
+
if (Array.isArray(data)) {
|
|
1149
|
+
return data.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
1150
|
+
}
|
|
1151
|
+
return data.byteLength;
|
|
1152
|
+
}
|
|
1153
|
+
function closeConnection(connection, code, reason) {
|
|
1154
|
+
if (connection.readyState === WebSocket.OPEN) {
|
|
1155
|
+
connection.close(code, reason);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1079
1158
|
var SocketService = class extends Emittery {
|
|
1080
1159
|
#server = null;
|
|
1081
1160
|
#httpServer = null;
|
|
@@ -1098,6 +1177,8 @@ var SocketService = class extends Emittery {
|
|
|
1098
1177
|
#heartbeatInterval = null;
|
|
1099
1178
|
#heartbeatTimeouts = /* @__PURE__ */ new Map();
|
|
1100
1179
|
#messageQueue = new MessageQueue();
|
|
1180
|
+
#messageRates = /* @__PURE__ */ new Map();
|
|
1181
|
+
#outboundMessageConfig = resolveOutboundMessageConfig(void 0);
|
|
1101
1182
|
/**
|
|
1102
1183
|
* Accesses the underlying ws server.
|
|
1103
1184
|
*/
|
|
@@ -1123,6 +1204,8 @@ var SocketService = class extends Emittery {
|
|
|
1123
1204
|
);
|
|
1124
1205
|
const websocketConfig = config.websocket;
|
|
1125
1206
|
const heartbeatConfig = resolveHeartbeatConfig(websocketConfig);
|
|
1207
|
+
const inboundMessageConfig = resolveInboundMessageConfig(websocketConfig);
|
|
1208
|
+
this.#outboundMessageConfig = resolveOutboundMessageConfig(websocketConfig);
|
|
1126
1209
|
try {
|
|
1127
1210
|
if (config.transport) {
|
|
1128
1211
|
this.#bus = new SocketBus(config.transport, {
|
|
@@ -1163,14 +1246,15 @@ var SocketService = class extends Emittery {
|
|
|
1163
1246
|
this.#configurePresenceFetcher();
|
|
1164
1247
|
}
|
|
1165
1248
|
const websocketServer = new WebSocketServer({
|
|
1166
|
-
noServer: true
|
|
1249
|
+
noServer: true,
|
|
1250
|
+
maxPayload: inboundMessageConfig.maxPayload
|
|
1167
1251
|
});
|
|
1168
1252
|
this.#server = websocketServer;
|
|
1169
1253
|
this.#httpServer = httpServer;
|
|
1170
1254
|
const upgrader = new SocketUpgrader(websocketServer, websocketConfig);
|
|
1171
1255
|
this.#upgradeHandler = (request, socket, head) => {
|
|
1172
1256
|
upgrader.handle(request, socket, head, (connection, upgradeRequest, accepted) => {
|
|
1173
|
-
this.#handleConnection(connection, upgradeRequest, accepted);
|
|
1257
|
+
this.#handleConnection(connection, upgradeRequest, accepted, inboundMessageConfig);
|
|
1174
1258
|
}).catch((error) => {
|
|
1175
1259
|
logger.warn("failed to upgrade socket connection: %s", error);
|
|
1176
1260
|
SocketUpgrader.reject(socket, 500, "Internal Server Error");
|
|
@@ -1186,7 +1270,7 @@ var SocketService = class extends Emittery {
|
|
|
1186
1270
|
throw error;
|
|
1187
1271
|
}
|
|
1188
1272
|
}
|
|
1189
|
-
#handleConnection(connection, request, upgrade) {
|
|
1273
|
+
#handleConnection(connection, request, upgrade, inboundMessageConfig) {
|
|
1190
1274
|
const socket = this.#wrapSocket(connection, request, upgrade);
|
|
1191
1275
|
connectChannel.traceSync(
|
|
1192
1276
|
() => {
|
|
@@ -1195,20 +1279,37 @@ var SocketService = class extends Emittery {
|
|
|
1195
1279
|
},
|
|
1196
1280
|
{ socketId: socket.id }
|
|
1197
1281
|
);
|
|
1282
|
+
connection.on("error", () => {
|
|
1283
|
+
});
|
|
1198
1284
|
connection.on("message", (data) => {
|
|
1285
|
+
if (getRawDataByteLength(data) > inboundMessageConfig.maxPayload) {
|
|
1286
|
+
closeConnection(connection, CLOSE_MESSAGE_TOO_BIG, "Message too big");
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
if (!this.#acceptMessageRate(socket.id, inboundMessageConfig)) {
|
|
1290
|
+
closeConnection(connection, CLOSE_POLICY_VIOLATION, "Socket message rate limit exceeded");
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1199
1293
|
const message = Message.fromTransport(data.toString());
|
|
1200
1294
|
if (message.type === "ping") {
|
|
1201
1295
|
sendFrame(socket.raw.connection, { id: message.id, type: "pong" });
|
|
1202
1296
|
return;
|
|
1203
1297
|
}
|
|
1204
|
-
this.#messageQueue.enqueue(
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1298
|
+
const enqueued = this.#messageQueue.enqueue(
|
|
1299
|
+
socket.id,
|
|
1300
|
+
async () => {
|
|
1301
|
+
await this.#handleMessage(socket, message).catch((error) => {
|
|
1302
|
+
sendFrame(connection, {
|
|
1303
|
+
type: "error",
|
|
1304
|
+
error: error instanceof Error ? error.message : "Unexpected socket error"
|
|
1305
|
+
});
|
|
1209
1306
|
});
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1307
|
+
},
|
|
1308
|
+
{ maxDepth: inboundMessageConfig.maxQueuedMessages }
|
|
1309
|
+
);
|
|
1310
|
+
if (!enqueued) {
|
|
1311
|
+
closeConnection(connection, CLOSE_POLICY_VIOLATION, "Socket message queue limit exceeded");
|
|
1312
|
+
}
|
|
1212
1313
|
});
|
|
1213
1314
|
connection.on("pong", () => {
|
|
1214
1315
|
this.#clearHeartbeatTimeout(socket.id);
|
|
@@ -1222,6 +1323,7 @@ var SocketService = class extends Emittery {
|
|
|
1222
1323
|
await disconnectChannel.tracePromise(async () => {
|
|
1223
1324
|
this.#clearHeartbeatTimeout(socket.id);
|
|
1224
1325
|
this.#messageQueue.delete(socket.id);
|
|
1326
|
+
this.#messageRates.delete(socket.id);
|
|
1225
1327
|
await this.#channelSubscriptions?.leaveAll(socket);
|
|
1226
1328
|
this.#channelSubscriptions?.deleteSocket(socket.id);
|
|
1227
1329
|
this.#sockets.delete(socket.id);
|
|
@@ -1306,8 +1408,7 @@ var SocketService = class extends Emittery {
|
|
|
1306
1408
|
#wrapSocket(connection, request, upgrade) {
|
|
1307
1409
|
const raw = {
|
|
1308
1410
|
id: randomUUID2(),
|
|
1309
|
-
data:
|
|
1310
|
-
auth: upgrade.auth,
|
|
1411
|
+
data: {},
|
|
1311
1412
|
connection,
|
|
1312
1413
|
request,
|
|
1313
1414
|
httpContext: upgrade.httpContext
|
|
@@ -1316,8 +1417,8 @@ var SocketService = class extends Emittery {
|
|
|
1316
1417
|
id: raw.id,
|
|
1317
1418
|
user: upgrade.user,
|
|
1318
1419
|
channels: /* @__PURE__ */ new Set(),
|
|
1319
|
-
emit(event, data) {
|
|
1320
|
-
|
|
1420
|
+
emit: (event, data) => {
|
|
1421
|
+
this.#sendEventFrame(connection, { type: "event", event, data });
|
|
1321
1422
|
},
|
|
1322
1423
|
joinUserRoom: (userId) => {
|
|
1323
1424
|
this.#joinUserRoom(raw.id, userRoom(userId));
|
|
@@ -1424,15 +1525,25 @@ var SocketService = class extends Emittery {
|
|
|
1424
1525
|
message.delivered = deliver();
|
|
1425
1526
|
}, message);
|
|
1426
1527
|
}
|
|
1528
|
+
#sendEventFrame(connection, frame) {
|
|
1529
|
+
return this.#sendSerializedEventFrame(connection, serializeFrame(frame));
|
|
1530
|
+
}
|
|
1531
|
+
#sendSerializedEventFrame(connection, serializedFrame) {
|
|
1532
|
+
return sendSerializedFrameWithBackpressure(
|
|
1533
|
+
connection,
|
|
1534
|
+
serializedFrame,
|
|
1535
|
+
this.#outboundMessageConfig
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1427
1538
|
#emitToChannelLocally(channel, event, data, except = []) {
|
|
1428
1539
|
let delivered = 0;
|
|
1540
|
+
const serializedFrame = serializeFrame({ type: "event", channel, event, data });
|
|
1429
1541
|
for (const socketId of this.#channelSubscriptions?.getSocketIds(channel) ?? []) {
|
|
1430
1542
|
if (except.includes(socketId)) {
|
|
1431
1543
|
continue;
|
|
1432
1544
|
}
|
|
1433
1545
|
const socket = this.#sockets.get(socketId);
|
|
1434
|
-
if (socket) {
|
|
1435
|
-
sendFrame(socket.raw.connection, { type: "event", channel, event, data });
|
|
1546
|
+
if (socket && this.#sendSerializedEventFrame(socket.raw.connection, serializedFrame)) {
|
|
1436
1547
|
delivered += 1;
|
|
1437
1548
|
}
|
|
1438
1549
|
}
|
|
@@ -1440,10 +1551,10 @@ var SocketService = class extends Emittery {
|
|
|
1440
1551
|
}
|
|
1441
1552
|
#emitToUserRoomLocally(room, event, data) {
|
|
1442
1553
|
let delivered = 0;
|
|
1554
|
+
const serializedFrame = serializeFrame({ type: "event", event, data });
|
|
1443
1555
|
for (const socketId of this.#userRooms.get(room) ?? []) {
|
|
1444
1556
|
const socket = this.#sockets.get(socketId);
|
|
1445
|
-
if (socket) {
|
|
1446
|
-
sendFrame(socket.raw.connection, { type: "event", event, data });
|
|
1557
|
+
if (socket && this.#sendSerializedEventFrame(socket.raw.connection, serializedFrame)) {
|
|
1447
1558
|
delivered += 1;
|
|
1448
1559
|
}
|
|
1449
1560
|
}
|
|
@@ -1451,9 +1562,11 @@ var SocketService = class extends Emittery {
|
|
|
1451
1562
|
}
|
|
1452
1563
|
#broadcastLocally(event, data) {
|
|
1453
1564
|
let delivered = 0;
|
|
1565
|
+
const serializedFrame = serializeFrame({ type: "event", event, data });
|
|
1454
1566
|
for (const socket of this.#sockets.values()) {
|
|
1455
|
-
|
|
1456
|
-
|
|
1567
|
+
if (this.#sendSerializedEventFrame(socket.raw.connection, serializedFrame)) {
|
|
1568
|
+
delivered += 1;
|
|
1569
|
+
}
|
|
1457
1570
|
}
|
|
1458
1571
|
return delivered;
|
|
1459
1572
|
}
|
|
@@ -1576,6 +1689,19 @@ var SocketService = class extends Emittery {
|
|
|
1576
1689
|
}
|
|
1577
1690
|
this.#heartbeatTimeouts.clear();
|
|
1578
1691
|
}
|
|
1692
|
+
#acceptMessageRate(socketId, config) {
|
|
1693
|
+
const now = Date.now();
|
|
1694
|
+
const rate = this.#messageRates.get(socketId);
|
|
1695
|
+
if (!rate || now - rate.intervalStartedAt >= config.messageRateInterval) {
|
|
1696
|
+
this.#messageRates.set(socketId, { count: 1, intervalStartedAt: now });
|
|
1697
|
+
return true;
|
|
1698
|
+
}
|
|
1699
|
+
if (rate.count >= config.maxMessagesPerInterval) {
|
|
1700
|
+
return false;
|
|
1701
|
+
}
|
|
1702
|
+
rate.count += 1;
|
|
1703
|
+
return true;
|
|
1704
|
+
}
|
|
1579
1705
|
#cleanup() {
|
|
1580
1706
|
this.#stopHeartbeat();
|
|
1581
1707
|
for (const socket of this.#sockets.values()) {
|
|
@@ -1585,6 +1711,7 @@ var SocketService = class extends Emittery {
|
|
|
1585
1711
|
this.#userRooms.clear();
|
|
1586
1712
|
this.#socketUserRoom.clear();
|
|
1587
1713
|
this.#messageQueue.clear();
|
|
1714
|
+
this.#messageRates.clear();
|
|
1588
1715
|
this.#channelSubscriptions?.clear();
|
|
1589
1716
|
}
|
|
1590
1717
|
async close() {
|
|
@@ -1791,4 +1918,4 @@ export {
|
|
|
1791
1918
|
SocketService,
|
|
1792
1919
|
ChannelRouter
|
|
1793
1920
|
};
|
|
1794
|
-
//# sourceMappingURL=chunk-
|
|
1921
|
+
//# sourceMappingURL=chunk-ILDK672E.js.map
|