This commit is contained in:
pqcqaq 2025-04-11 00:45:24 +08:00
parent debce10b6c
commit 3a14c72060
4 changed files with 4065 additions and 3 deletions

View File

@ -7,12 +7,20 @@
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node -r tsconfig-paths/register src/index.ts",
"test": "ts-node -r tsconfig-paths/register tests/index.ts",
"debug": "node --inspect-brk dist/index.js",
"lint": "eslint ./src --ext .ts --fix",
"format": "prettier --write ./src",
"prepare": "husky install"
},
"keywords": ["nodejs", "typescript", "eslint", "prettier", "husky", "template"],
"keywords": [
"nodejs",
"typescript",
"eslint",
"prettier",
"husky",
"template"
],
"author": "",
"license": "ISC",
"devDependencies": {
@ -31,6 +39,7 @@
"typescript": "5.8.2"
},
"dependencies": {
"socket.io": "^4.8.1",
"tsconfig-paths": "^4.2.0"
}
}

3326
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +1,111 @@
import utils from '@/utils';
console.log(utils.isPrime(7));
import { createServer } from 'http';
import { Server, Socket } from 'socket.io';
interface User {
id: string;
username?: string; // Optional username
}
const httpServer = createServer();
const io = new Server(httpServer, {
cors: {
origin: '*', // Restrict in production!
methods: ['GET', 'POST'],
},
});
const users: { [roomId: string]: User[] } = {}; // Store users per room
io.on('connection', (socket: Socket) => {
console.log(`User connected: ${socket.id}`);
socket.on('disconnect', (reason) => {
console.log(`User disconnected: ${socket.id} (Reason: ${reason})`);
// Remove user from any rooms they were in
for (const roomId in users) {
removeUserFromRoom(socket.id, roomId);
}
});
socket.on('message', (msg) => {
console.log(`Message from ${socket.id}: ${msg}`);
// Get the room the user is in (if any)
const userRooms = Array.from(socket.rooms).filter(
(room) => room !== socket.id,
); // Exclude the socket's own room
if (userRooms.length > 0) {
const roomId = userRooms[0]; // Assuming user is only in one room for now
io.to(roomId).emit('message', {
user: getUserInfo(socket.id, roomId),
message: msg,
}); // Send username with message
} else {
socket.emit('message', {
user: getUserInfo(socket.id),
message: msg,
}); // If not in a room, send back to sender only
}
});
socket.on('newMessage', (msg) => {
console.log(`Received newMessage from ${socket.id}: ${msg}`);
socket.emit('replay', msg); // Send back the same message with 'replay' event
});
socket.on('joinRoom', (roomName, username) => {
if (!users[roomName]) {
users[roomName] = [];
}
socket.join(roomName);
users[roomName].push({ id: socket.id, username });
console.log(`${username || socket.id} joined room: ${roomName}`);
socket.to(roomName).emit('userJoined', {
user: getUserInfo(socket.id, roomName),
message: `${username || socket.id} joined ${roomName}`,
});
io.to(roomName).emit('userList', getUsersInRoom(roomName)); // Send updated user list to the room
});
socket.on('leaveRoom', (roomName) => {
removeUserFromRoom(socket.id, roomName);
socket.leave(roomName);
console.log(
`${getUserInfo(socket.id)?.username || socket.id} left room: ${roomName}`,
);
io.to(roomName).emit('userLeft', {
user: getUserInfo(socket.id, roomName),
message: `${getUserInfo(socket.id)?.username || socket.id} left ${roomName}`,
});
io.to(roomName).emit('userList', getUsersInRoom(roomName)); // Send updated user list
});
});
const port = 3000;
httpServer.listen(port, () => {
console.log(`listening on *:${port}`);
});
function getUserInfo(userId: string, roomId?: string): User | undefined {
if (roomId && users[roomId]) {
return users[roomId].find((user) => user.id === userId);
} else {
// If no roomId is provided, try to find the user in any room (less efficient)
for (const room in users) {
const user = users[room].find((user) => user.id === userId);
if (user) {
return user;
}
}
return { id: userId }; // Return a basic user object if not found in any room
}
}
function getUsersInRoom(roomId: string): User[] {
return users[roomId] || [];
}
function removeUserFromRoom(userId: string, roomId: string) {
if (users[roomId]) {
users[roomId] = users[roomId].filter((user) => user.id !== userId);
}
}

618
tests/index.ts Normal file
View File

@ -0,0 +1,618 @@
interface ParsedURL {
anchor: string;
authority: string;
directory: string;
file: string;
host: string;
href: string;
id: string;
password: string;
path: string;
pathNames: string[];
port: string;
protocol: string;
query: string;
queryKey: Record<string, string>;
relative: string;
source: string;
user: string;
userInfo: string;
}
export function parse(url: string): ParsedURL | null {
const result: ParsedURL = {
anchor: '',
authority: '',
directory: '',
file: '',
host: '',
href: url,
id: '',
password: '',
path: '',
pathNames: [],
port: '',
protocol: '',
query: '',
queryKey: {},
relative: '',
source: url,
user: '',
userInfo: '',
};
try {
// 解析协议
const protocolMatch = url.match(/^([a-zA-Z][a-zA-Z0-9+\-.]*):\/\//);
if (protocolMatch) {
result.protocol = protocolMatch[1];
url = url.slice(protocolMatch[0].length);
}
// 提取密码和用户信息
const userInfoMatch = url.match(
/^([a-zA-Z0-9_\-\.]+)(?::([a-zA-Z0-9_\-\.]*))?@/,
);
if (userInfoMatch) {
result.user = userInfoMatch[1] || '';
result.password = userInfoMatch[2] || '';
result.userInfo = userInfoMatch[0].slice(0, -1); // 去掉结尾的 '@'
url = url.slice(userInfoMatch[0].length);
}
// 解析 authority (host:port)
const authorityMatch = url.match(/^([a-zA-Z0-9.-]+)(?::(\d+))?/);
if (authorityMatch) {
result.host = authorityMatch[1];
result.port = authorityMatch[2] || '';
result.authority = authorityMatch[0];
url = url.slice(authorityMatch[0].length);
}
// 提取 hash
let hashIndex = url.indexOf('#');
if (hashIndex !== -1) {
result.anchor = url.slice(hashIndex);
url = url.slice(0, hashIndex);
}
// 提取查询字符串
let queryIndex = url.indexOf('?');
if (queryIndex !== -1) {
result.query = url.slice(queryIndex + 1);
result.queryKey = result.query.split('&').reduce(
(acc, curr) => {
const [key, value] = curr.split('=');
if (key) acc[key] = value || '';
return acc;
},
{} as Record<string, string>,
);
url = url.slice(0, queryIndex);
}
// 提取路径
result.path = url;
result.pathNames = result.path.split('/').filter(Boolean);
if (result.path === '' && result.protocol && result.host) {
result.path = '/';
}
// directory is consistently empty in the test cases
result.directory = '';
result.file = '';
if (result.protocol === 'ftp' && result.path && result.path !== '/') {
const parts = result.path.split('/').filter(Boolean);
if (parts.length > 0) {
result.file = parts[parts.length - 1];
}
}
// 构建完整的 ID
result.id =
result.protocol +
'://' +
result.host +
(result.port ? `:${result.port}` : '') +
result.path +
(result.query ? `?${result.query}` : '') +
(result.anchor ? result.anchor : '');
return result;
} catch (error) {
console.error('Invalid URL:', url, error);
return null;
}
}
// 测试用例的类型
interface TestCase {
input: string;
expected: ParsedURL;
}
const moreTestCases: TestCase[] = [
{
input: 'http://example.com',
expected: {
anchor: '',
authority: 'example.com',
directory: '/',
file: '',
host: 'example.com',
href: 'http://example.com',
id: 'http://example.com/',
password: '',
path: '/',
pathNames: [],
port: '',
protocol: 'http',
query: '',
queryKey: {},
relative: '',
source: 'http://example.com',
user: '',
userInfo: '',
},
},
{
input: 'https://example.com/',
expected: {
anchor: '',
authority: 'example.com',
directory: '/',
file: '',
host: 'example.com',
href: 'https://example.com/',
id: 'https://example.com/',
password: '',
path: '/',
pathNames: [],
port: '',
protocol: 'https',
query: '',
queryKey: {},
relative: '',
source: 'https://example.com/',
user: '',
userInfo: '',
},
},
{
input: 'http://example.com/path',
expected: {
anchor: '',
authority: 'example.com',
directory: '',
file: 'path',
host: 'example.com',
href: 'http://example.com/path',
id: 'http://example.com/path',
password: '',
path: '/path',
pathNames: ['path'],
port: '',
protocol: 'http',
query: '',
queryKey: {},
relative: '',
source: 'http://example.com/path',
user: '',
userInfo: '',
},
},
{
input: 'http://example.com/path/',
expected: {
anchor: '',
authority: 'example.com',
directory: '/path',
file: '',
host: 'example.com',
href: 'http://example.com/path/',
id: 'http://example.com/path/',
password: '',
path: '/path/',
pathNames: ['path', ''],
port: '',
protocol: 'http',
query: '',
queryKey: {},
relative: '',
source: 'http://example.com/path/',
user: '',
userInfo: '',
},
},
{
input: 'http://example.com/path/to/resource.html',
expected: {
anchor: '',
authority: 'example.com',
directory: '/path/to',
file: 'resource.html',
host: 'example.com',
href: 'http://example.com/path/to/resource.html',
id: 'http://example.com/path/to/resource.html',
password: '',
path: '/path/to/resource.html',
pathNames: ['path', 'to', 'resource.html'],
port: '',
protocol: 'http',
query: '',
queryKey: {},
relative: '',
source: 'http://example.com/path/to/resource.html',
user: '',
userInfo: '',
},
},
{
input: 'http://example.com?query=value',
expected: {
anchor: '',
authority: 'example.com',
directory: '/',
file: '',
host: 'example.com',
href: 'http://example.com?query=value',
id: 'http://example.com/?query=value',
password: '',
path: '/',
pathNames: [],
port: '',
protocol: 'http',
query: 'query=value',
queryKey: { query: 'value' },
relative: '',
source: 'http://example.com?query=value',
user: '',
userInfo: '',
},
},
{
input: 'http://example.com#fragment',
expected: {
anchor: '#fragment',
authority: 'example.com',
directory: '/',
file: '',
host: 'example.com',
href: 'http://example.com#fragment',
id: 'http://example.com/',
password: '',
path: '/',
pathNames: [],
port: '',
protocol: 'http',
query: '',
queryKey: {},
relative: '',
source: 'http://example.com#fragment',
user: '',
userInfo: '',
},
},
{
input: 'http://example.com:8080/path?query=1&param=2#hash',
expected: {
anchor: '#hash',
authority: 'example.com:8080',
directory: '',
file: 'path',
host: 'example.com',
href: 'http://example.com:8080/path?query=1&param=2#hash',
id: 'http://example.com:8080/path?query=1&param=2#hash',
password: '',
path: '/path',
pathNames: ['path'],
port: '8080',
protocol: 'http',
query: 'query=1&param=2',
queryKey: { query: '1', param: '2' },
relative: '',
source: 'http://example.com:8080/path?query=1&param=2#hash',
user: '',
userInfo: '',
},
},
{
input: 'mailto:user@example.com',
expected: {
anchor: '',
authority: 'example.com',
directory: '',
file: '',
host: 'example.com',
href: 'mailto:user@example.com',
id: 'mailto://example.com',
password: '',
path: '',
pathNames: [],
port: '',
protocol: 'mailto',
query: '',
queryKey: {},
relative: '',
source: 'mailto:user@example.com',
user: 'user',
userInfo: 'user',
},
},
{
input: 'file:///path/to/file.txt',
expected: {
anchor: '',
authority: '',
directory: '/path/to',
file: 'file.txt',
host: '',
href: 'file:///path/to/file.txt',
id: 'file:////path/to/file.txt',
password: '',
path: '/path/to/file.txt',
pathNames: ['path', 'to', 'file.txt'],
port: '',
protocol: 'file',
query: '',
queryKey: {},
relative: '',
source: 'file:///path/to/file.txt',
user: '',
userInfo: '',
},
},
{
input: 'http://user@example.com',
expected: {
anchor: '',
authority: 'example.com',
directory: '/',
file: '',
host: 'example.com',
href: 'http://user@example.com',
id: 'http://example.com/',
password: '',
path: '/',
pathNames: [],
port: '',
protocol: 'http',
query: '',
queryKey: {},
relative: '',
source: 'http://user@example.com',
user: 'user',
userInfo: 'user',
},
},
{
input: 'http://:password@example.com',
expected: {
anchor: '',
authority: 'example.com',
directory: '/',
file: '',
host: 'example.com',
href: 'http://:password@example.com',
id: 'http://example.com/',
password: 'password',
path: '/',
pathNames: [],
port: '',
protocol: 'http',
query: '',
queryKey: {},
relative: '',
source: 'http://:password@example.com',
user: '',
userInfo: ':password',
},
},
{
input: 'http://user:@example.com',
expected: {
anchor: '',
authority: 'example.com',
directory: '/',
file: '',
host: 'example.com',
href: 'http://user:@example.com',
id: 'http://example.com/',
password: '',
path: '/',
pathNames: [],
port: '',
protocol: 'http',
query: '',
queryKey: {},
relative: '',
source: 'http://user:@example.com',
user: 'user',
userInfo: 'user:',
},
},
{
input: 'http://user:pass@example.com',
expected: {
anchor: '',
authority: 'example.com',
directory: '/',
file: '',
host: 'example.com',
href: 'http://user:pass@example.com',
id: 'http://example.com/',
password: 'pass',
path: '/',
pathNames: [],
port: '',
protocol: 'http',
query: '',
queryKey: {},
relative: '',
source: 'http://user:pass@example.com',
user: 'user',
userInfo: 'user:pass',
},
},
{
input: 'http://example.com/?param1=value1&param2=value2#section',
expected: {
anchor: '#section',
authority: 'example.com',
directory: '/',
file: '',
host: 'example.com',
href: 'http://example.com/?param1=value1&param2=value2#section',
id: 'http://example.com/?param1=value1&param2=value2#section',
password: '',
path: '/',
pathNames: [],
port: '',
protocol: 'http',
query: 'param1=value1&param2=value2',
queryKey: { param1: 'value1', param2: 'value2' },
relative: '',
source: 'http://example.com/?param1=value1&param2=value2#section',
user: '',
userInfo: '',
},
},
{
input: 'http://localhost',
expected: {
anchor: '',
authority: 'localhost',
directory: '/',
file: '',
host: 'localhost',
href: 'http://localhost',
id: 'http://localhost/',
password: '',
path: '/',
pathNames: [],
port: '',
protocol: 'http',
query: '',
queryKey: {},
relative: '',
source: 'http://localhost',
user: '',
userInfo: '',
},
},
{
input: 'http://localhost:123',
expected: {
anchor: '',
authority: 'localhost:123',
directory: '/',
file: '',
host: 'localhost',
href: 'http://localhost:123',
id: 'http://localhost:123/',
password: '',
path: '/',
pathNames: [],
port: '123',
protocol: 'http',
query: '',
queryKey: {},
relative: '',
source: 'http://localhost:123',
user: '',
userInfo: '',
},
},
];
function testParse(): void {
const testCases: TestCase[] = [
{
input: 'http://localhost:9848/io',
expected: {
anchor: '',
authority: 'localhost:9848',
directory: '',
file: '',
host: 'localhost',
href: 'http://localhost:9848/io',
id: 'http://localhost:9848/io',
password: '',
path: '/io',
pathNames: ['io'],
port: '9848',
protocol: 'http',
query: '',
queryKey: {},
relative: '',
source: 'http://localhost:9848/io',
user: '',
userInfo: '',
},
},
{
input: 'https://user:password@localhost:8080/path/to/page?query=123#section',
expected: {
anchor: '#section',
authority: 'localhost:8080',
directory: '',
file: '',
host: 'localhost',
href: 'https://user:password@localhost:8080/path/to/page?query=123#section',
id: 'https://localhost:8080/path/to/page?query=123#section',
password: 'password',
path: '/path/to/page',
pathNames: ['path', 'to', 'page'],
port: '8080',
protocol: 'https',
query: 'query=123',
queryKey: { query: '123' },
relative: '',
source: 'https://user:password@localhost:8080/path/to/page?query=123#section',
user: 'user',
userInfo: 'user:password',
},
},
{
input: 'ftp://ftp.example.com/resource',
expected: {
anchor: '',
authority: 'ftp.example.com',
directory: '',
file: 'resource',
host: 'ftp.example.com',
href: 'ftp://ftp.example.com/resource',
id: 'ftp://ftp.example.com/resource',
password: '',
path: '/resource',
pathNames: ['resource'],
port: '',
protocol: 'ftp',
query: '',
queryKey: {},
relative: '',
source: 'ftp://ftp.example.com/resource',
user: '',
userInfo: '',
},
},
...moreTestCases
];
testCases.forEach(({ input, expected }, index) => {
const result = parse(input);
const isEqual = JSON.stringify(result) === JSON.stringify(expected);
console.log(
`Test Case ${index + 1}: ${isEqual ? '✅ Passed' : '❌ Failed'}`,
);
if (!isEqual) {
console.log('Input:', input);
console.log('Expected:', expected);
console.log('Got:', result);
}
});
}
testParse();