rename websocket directory to websocket-chat
This commit is contained in:
24
Web/WebSocket/websocket-chat/apps/frontend-react/.gitignore
vendored
Normal file
24
Web/WebSocket/websocket-chat/apps/frontend-react/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
2
Web/WebSocket/websocket-chat/apps/frontend-react/.npmrc
Normal file
2
Web/WebSocket/websocket-chat/apps/frontend-react/.npmrc
Normal file
@@ -0,0 +1,2 @@
|
||||
save-exact=true
|
||||
package-lock=false
|
||||
1
Web/WebSocket/websocket-chat/apps/frontend-react/.nvmrc
Normal file
1
Web/WebSocket/websocket-chat/apps/frontend-react/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
24
|
||||
12
Web/WebSocket/websocket-chat/apps/frontend-react/.prettierrc
Normal file
12
Web/WebSocket/websocket-chat/apps/frontend-react/.prettierrc
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"bracketSameLine": true,
|
||||
"bracketSpacing": false,
|
||||
"printWidth": 160,
|
||||
"semi": true,
|
||||
"singleAttributePerLine": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"useTabs": false
|
||||
}
|
||||
73
Web/WebSocket/websocket-chat/apps/frontend-react/README.md
Normal file
73
Web/WebSocket/websocket-chat/apps/frontend-react/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is currently not compatible with SWC. See [this issue](https://github.com/vitejs/vite-plugin-react/issues/428) for tracking the progress.
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname
|
||||
}
|
||||
// other options...
|
||||
}
|
||||
}
|
||||
]);
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x';
|
||||
import reactDom from 'eslint-plugin-react-dom';
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname
|
||||
}
|
||||
// other options...
|
||||
}
|
||||
}
|
||||
]);
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
import js from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import {defineConfig, globalIgnores} from 'eslint/config';
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [js.configs.recommended, tseslint.configs.recommended, reactHooks.configs['recommended-latest'], reactRefresh.configs.vite],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser
|
||||
}
|
||||
}
|
||||
]);
|
||||
13
Web/WebSocket/websocket-chat/apps/frontend-react/index.html
Normal file
13
Web/WebSocket/websocket-chat/apps/frontend-react/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend-react</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "frontend-react",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "2.9.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-redux": "9.2.0",
|
||||
"styled-components": "6.1.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@techniker-me/websocket-shared-types": "*",
|
||||
"@types/react": "^19.1.13",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@vitejs/plugin-react-swc": "^4.1.0",
|
||||
"babel-plugin-styled-components": "2.1.4",
|
||||
"babel-plugin-transform-amd-to-commonjs": "1.6.0",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.4.0",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.44.0",
|
||||
"vite": "^7.1.7",
|
||||
"vite-plugin-babel": "1.3.2"
|
||||
}
|
||||
}
|
||||
10
Web/WebSocket/websocket-chat/apps/frontend-react/src/App.tsx
Normal file
10
Web/WebSocket/websocket-chat/apps/frontend-react/src/App.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import {LoginView} from './views';
|
||||
|
||||
export default function App() {
|
||||
console.log('APP')
|
||||
return (
|
||||
<>
|
||||
<LoginView />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface ILoginFormProps {
|
||||
onSubmit: (username: string, secret: string) => void;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useState } from 'react';
|
||||
import ILoginFormPops from './ILoginFormProps';
|
||||
import { Input } from './Styled';
|
||||
|
||||
export function LoginForm({ onSubmit }: ILoginFormProps) {
|
||||
const [username, setUsername] = useState<string>('');
|
||||
const [secret, setSecret] = useState<string>('');
|
||||
|
||||
// The submit handler now correctly uses 'event'
|
||||
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
onSubmit(username, secret);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
{/*
|
||||
- The 'placeholder' prop is now lowercase.
|
||||
- The 'onChange' handler correctly destructures {target}.
|
||||
*/}
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
autoComplete="username"
|
||||
value={username}
|
||||
onChange={({ target }) => setUsername(target.value)}
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
autoComplete="current-password"
|
||||
value={secret}
|
||||
onChange={({ target }) => setSecret(target.value)}
|
||||
/>
|
||||
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Input = styled.input`
|
||||
width: 100%;
|
||||
heght: 1.3rem;
|
||||
backgroundColor: blue
|
||||
`
|
||||
|
||||
export const Button = styled.button`
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
background-color: #a7c42;
|
||||
|
||||
`
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './ILoginFormProps';
|
||||
export * from './LoginForm';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './LoginForm';
|
||||
@@ -0,0 +1,68 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import {StrictMode} from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import {Provider} from 'react-redux';
|
||||
import store from './store';
|
||||
import App from './App.tsx';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<Provider store={store}>
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
</Provider>
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
import {configureStore} from '@reduxjs/toolkit';
|
||||
import {userSlice} from './slices';
|
||||
|
||||
// Configure the main store
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
// Add the reducer from your slice to the store
|
||||
user: userSlice.reducer
|
||||
}
|
||||
});
|
||||
|
||||
export default store;
|
||||
|
||||
// Type definition for the root state, can be used with useSelector hook
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
|
||||
// Type definition for dispatch, can be used with useDispatch hook
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
@@ -0,0 +1,40 @@
|
||||
import {createSlice, configureStore, PayloadAction} from '@reduxjs/toolkit';
|
||||
|
||||
interface IUserState {
|
||||
id: string | null;
|
||||
name: string | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const initialState: IUserState = {
|
||||
id: null,
|
||||
name: null,
|
||||
isLoading: false,
|
||||
error: null
|
||||
};
|
||||
|
||||
export const userSlice = createSlice({
|
||||
name: 'user', // A name for this slice, used in action types
|
||||
initialState,
|
||||
reducers: {
|
||||
// The key here becomes part of the action type: 'user/setUserLoading'
|
||||
setUserLoading(state, action: PayloadAction<boolean>) {
|
||||
// Redux Toolkit uses Immer, so you can "mutate" the state directly.
|
||||
// Immer handles the immutable update behind the scenes.
|
||||
state.isLoading = action.payload;
|
||||
},
|
||||
userFetchSuccess(state, action: PayloadAction<{id: string; name: string}>) {
|
||||
state.isLoading = false;
|
||||
state.id = action.payload.id;
|
||||
state.name = action.payload.name;
|
||||
state.error = null;
|
||||
},
|
||||
userFetchError(state, action: PayloadAction<string>) {
|
||||
state.isLoading = false;
|
||||
state.error = action.payload;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const {setUserLoading, userFetchSuccess, userFetchError} = userSlice.actions;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './User.slice';
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
import {LoginForm} from '../../components';
|
||||
|
||||
export function LoginView() {
|
||||
const handleLoginSubmit = (username: string, secret: string) => {
|
||||
console.log('handling login submit username [%o] secret [%o]', username, secret)
|
||||
};
|
||||
|
||||
console.log('LoginView...')
|
||||
|
||||
return <>
|
||||
<LoginForm onSubmit={handleLoginSubmit} />
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './LoginView';
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [{"path": "./tsconfig.app.json"}, {"path": "./tsconfig.node.json"}]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"types": [],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import type {UserConfig} from 'vite';
|
||||
|
||||
import path from 'node:path';
|
||||
import {defineConfig} from 'vite';
|
||||
import VitePluginReact from '@vitejs/plugin-react-swc';
|
||||
import ViteBabelPlugin from 'vite-plugin-babel';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(({mode}) => {
|
||||
const isProductionMode = mode === 'production';
|
||||
|
||||
// optional: const modeDependentEnvironmentVariables = loadEnv(mode, process.cwd()); // https://vite.dev/config/#using-environment-variables-in-config
|
||||
return {
|
||||
define: {},
|
||||
root: path.resolve(process.cwd()),
|
||||
publicDir: 'public',
|
||||
cacheDir: 'node_modules/.vite', // default is `node_modules/.vite` <-- https://vite.dev/config/shared-options.html#cachedir
|
||||
base: '/',
|
||||
mode,
|
||||
resolve: {
|
||||
alias: {
|
||||
assets: path.resolve(process.cwd(), 'src/assets'),
|
||||
components: path.resolve(process.cwd(), 'src/components'),
|
||||
services: path.resolve(process.cwd(), 'src/services'),
|
||||
store: path.resolve(process.cwd(), 'src/store'),
|
||||
styles: path.resolve(process.cwd(), 'src/styles'),
|
||||
themes: path.resolve(process.cwd(), 'src/themes'),
|
||||
utils: path.resolve(process.cwd(), 'src/utils'),
|
||||
views: path.resolve(process.cwd(), 'src/views')
|
||||
},
|
||||
dedupe: [],
|
||||
conditions: ['module', 'browser', 'development|production'], // default is `['module', 'browser', 'development|production']`
|
||||
mainFields: ['browser', 'module'], // default is `['browser', 'module', 'jsnext:main', 'jsnext']`
|
||||
extensions: ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'],
|
||||
preserveSymlinks: false // default is false
|
||||
},
|
||||
css: {
|
||||
modules: undefined, // default is `undefined`
|
||||
// postcss: {},
|
||||
devSourcemap: true, // default is `false` **experimental Vite feature**
|
||||
transformer: 'postcss' // default is `postcss` **experimental Vite feature**
|
||||
},
|
||||
json: {
|
||||
stringify: true // default is 'auto' // https://vite.dev/config/shared-options.html#json-stringify
|
||||
},
|
||||
esbuild: {
|
||||
jsxFactory: 'h',
|
||||
jsxFragment: 'Fragment',
|
||||
jsxInject: `import React from 'react'`
|
||||
},
|
||||
logLevel: 'debug', // default is `info`
|
||||
clearScreen: false, // default is `true`
|
||||
envDir: '.',
|
||||
appType: 'spa',
|
||||
server: {
|
||||
host: 'localhost',
|
||||
allowedHosts: ['localhost', '.phenixrts.com'],
|
||||
port: 5173,
|
||||
open: true,
|
||||
hmr: true,
|
||||
fs: {
|
||||
strict: true,
|
||||
deny: ['.env', '.env.*', '*.{crt,pem}', '**/.git/**'] // default is `['.env', '.env.*', '*.{crt,pem}', '**/.git/**']`
|
||||
}
|
||||
},
|
||||
build: {
|
||||
target: ['es2020', 'chrome80', 'firefox78', 'safari14'],
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets',
|
||||
assetsInlineLimit: 4096, // default is `4096` ~ 4KiB
|
||||
cssCodeSplit: true,
|
||||
minify: isProductionMode,
|
||||
cssMinify: isProductionMode,
|
||||
sourcemap: !isProductionMode,
|
||||
manifest: false, // default is `false`<-- https://vite.dev/config/build-options.html#build-manifest ,
|
||||
ssrManifest: false,
|
||||
ssr: false,
|
||||
emitAssets: false,
|
||||
terserOptions: {},
|
||||
emptyOutDir: true,
|
||||
copyPublicDir: true,
|
||||
reportCompressedSize: true,
|
||||
chunkSizeWarningLimit: 500,
|
||||
watch: null
|
||||
},
|
||||
plugins: [
|
||||
VitePluginReact(),
|
||||
ViteBabelPlugin({
|
||||
babelConfig: {
|
||||
plugins: ['transform-amd-to-commonjs', 'babel-plugin-styled-components']
|
||||
}
|
||||
})
|
||||
]
|
||||
} satisfies UserConfig;
|
||||
});
|
||||
135
Web/WebSocket/websocket-chat/apps/frontend/bun.lock
Normal file
135
Web/WebSocket/websocket-chat/apps/frontend/bun.lock
Normal file
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "websocket",
|
||||
"devDependencies": {
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.1.7",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.10", "", { "os": "aix", "cpu": "ppc64" }, "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.10", "", { "os": "android", "cpu": "arm" }, "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.10", "", { "os": "android", "cpu": "arm64" }, "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.10", "", { "os": "android", "cpu": "x64" }, "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.10", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.10", "", { "os": "linux", "cpu": "arm" }, "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.10", "", { "os": "linux", "cpu": "ia32" }, "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.10", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.10", "", { "os": "linux", "cpu": "s390x" }, "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.10", "", { "os": "none", "cpu": "x64" }, "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.10", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.10", "", { "os": "openbsd", "cpu": "x64" }, "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.10", "", { "os": "sunos", "cpu": "x64" }, "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.10", "", { "os": "win32", "cpu": "ia32" }, "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.10", "", { "os": "win32", "cpu": "x64" }, "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.3", "", { "os": "android", "cpu": "arm" }, "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.3", "", { "os": "android", "cpu": "arm64" }, "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.3", "", { "os": "linux", "cpu": "arm" }, "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.3", "", { "os": "linux", "cpu": "arm" }, "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.3", "", { "os": "linux", "cpu": "none" }, "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.3", "", { "os": "linux", "cpu": "none" }, "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.3", "", { "os": "linux", "cpu": "none" }, "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.3", "", { "os": "linux", "cpu": "x64" }, "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.3", "", { "os": "none", "cpu": "arm64" }, "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.3", "", { "os": "win32", "cpu": "x64" }, "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.3", "", { "os": "win32", "cpu": "x64" }, "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"rollup": ["rollup@4.52.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.3", "@rollup/rollup-android-arm64": "4.52.3", "@rollup/rollup-darwin-arm64": "4.52.3", "@rollup/rollup-darwin-x64": "4.52.3", "@rollup/rollup-freebsd-arm64": "4.52.3", "@rollup/rollup-freebsd-x64": "4.52.3", "@rollup/rollup-linux-arm-gnueabihf": "4.52.3", "@rollup/rollup-linux-arm-musleabihf": "4.52.3", "@rollup/rollup-linux-arm64-gnu": "4.52.3", "@rollup/rollup-linux-arm64-musl": "4.52.3", "@rollup/rollup-linux-loong64-gnu": "4.52.3", "@rollup/rollup-linux-ppc64-gnu": "4.52.3", "@rollup/rollup-linux-riscv64-gnu": "4.52.3", "@rollup/rollup-linux-riscv64-musl": "4.52.3", "@rollup/rollup-linux-s390x-gnu": "4.52.3", "@rollup/rollup-linux-x64-gnu": "4.52.3", "@rollup/rollup-linux-x64-musl": "4.52.3", "@rollup/rollup-openharmony-arm64": "4.52.3", "@rollup/rollup-win32-arm64-msvc": "4.52.3", "@rollup/rollup-win32-ia32-msvc": "4.52.3", "@rollup/rollup-win32-x64-gnu": "4.52.3", "@rollup/rollup-win32-x64-msvc": "4.52.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"vite": ["vite@7.1.7", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA=="],
|
||||
}
|
||||
}
|
||||
13
Web/WebSocket/websocket-chat/apps/frontend/index.html
Normal file
13
Web/WebSocket/websocket-chat/apps/frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>websocket</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
324
Web/WebSocket/websocket-chat/apps/frontend/package-lock.json
generated
Normal file
324
Web/WebSocket/websocket-chat/apps/frontend/package-lock.json
generated
Normal file
@@ -0,0 +1,324 @@
|
||||
{
|
||||
"name": "websocket",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "websocket",
|
||||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.1.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.10",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.52.3",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.10",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.10",
|
||||
"@esbuild/android-arm": "0.25.10",
|
||||
"@esbuild/android-arm64": "0.25.10",
|
||||
"@esbuild/android-x64": "0.25.10",
|
||||
"@esbuild/darwin-arm64": "0.25.10",
|
||||
"@esbuild/darwin-x64": "0.25.10",
|
||||
"@esbuild/freebsd-arm64": "0.25.10",
|
||||
"@esbuild/freebsd-x64": "0.25.10",
|
||||
"@esbuild/linux-arm": "0.25.10",
|
||||
"@esbuild/linux-arm64": "0.25.10",
|
||||
"@esbuild/linux-ia32": "0.25.10",
|
||||
"@esbuild/linux-loong64": "0.25.10",
|
||||
"@esbuild/linux-mips64el": "0.25.10",
|
||||
"@esbuild/linux-ppc64": "0.25.10",
|
||||
"@esbuild/linux-riscv64": "0.25.10",
|
||||
"@esbuild/linux-s390x": "0.25.10",
|
||||
"@esbuild/linux-x64": "0.25.10",
|
||||
"@esbuild/netbsd-arm64": "0.25.10",
|
||||
"@esbuild/netbsd-x64": "0.25.10",
|
||||
"@esbuild/openbsd-arm64": "0.25.10",
|
||||
"@esbuild/openbsd-x64": "0.25.10",
|
||||
"@esbuild/openharmony-arm64": "0.25.10",
|
||||
"@esbuild/sunos-x64": "0.25.10",
|
||||
"@esbuild/win32-arm64": "0.25.10",
|
||||
"@esbuild/win32-ia32": "0.25.10",
|
||||
"@esbuild/win32-x64": "0.25.10"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.52.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.52.3",
|
||||
"@rollup/rollup-android-arm64": "4.52.3",
|
||||
"@rollup/rollup-darwin-arm64": "4.52.3",
|
||||
"@rollup/rollup-darwin-x64": "4.52.3",
|
||||
"@rollup/rollup-freebsd-arm64": "4.52.3",
|
||||
"@rollup/rollup-freebsd-x64": "4.52.3",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.52.3",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.52.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.52.3",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.52.3",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.52.3",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.52.3",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.52.3",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.52.3",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.52.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.52.3",
|
||||
"@rollup/rollup-linux-x64-musl": "4.52.3",
|
||||
"@rollup/rollup-openharmony-arm64": "4.52.3",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.52.3",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.52.3",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.52.3",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.52.3",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.1.7",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"rollup": "^4.43.0",
|
||||
"tinyglobby": "^0.2.15"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Web/WebSocket/websocket-chat/apps/frontend/package.json
Normal file
22
Web/WebSocket/websocket-chat/apps/frontend/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@techniker-me/websocket-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc",
|
||||
"clean": "rm -rf dist",
|
||||
"lint": "echo 'No linting configured for frontend'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "5.9.2",
|
||||
"vite": "7.1.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@techniker-me/logger": "0.0.15",
|
||||
"@techniker-me/tools": "2025.0.16"
|
||||
}
|
||||
}
|
||||
96
Web/WebSocket/websocket-chat/apps/frontend/src/main.ts
Normal file
96
Web/WebSocket/websocket-chat/apps/frontend/src/main.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import WebSocketConnection from './websockets/WebSocketConnection';
|
||||
import {WebSocketConnectionStatus} from './websockets/WebSocketConnectionStatus';
|
||||
import './styles.css';
|
||||
import {DisposableList} from '@techniker-me/tools';
|
||||
|
||||
const disposables = new DisposableList();
|
||||
let websocket = new WebSocketConnection('ws://' + window.location.hostname + ':3000/ws');
|
||||
|
||||
let username = '';
|
||||
|
||||
const appContainer = document.getElementById('app') as HTMLDivElement;
|
||||
const chatContainer = document.createElement('div');
|
||||
chatContainer.id = 'chat-container';
|
||||
|
||||
const websocketStatusContainer = document.createElement('div');
|
||||
websocketStatusContainer.id = 'websocket-status-container';
|
||||
|
||||
const rttContainer = document.createElement('div');
|
||||
rttContainer.id = 'rtt-container';
|
||||
rttContainer.innerHTML = `RTT: [<span id="rtt-value">0</span>] milliseconds`;
|
||||
|
||||
const messagesElement = document.createElement('div');
|
||||
messagesElement.id = 'messages-container';
|
||||
|
||||
const messageInput = document.createElement('input');
|
||||
messageInput.type = 'text';
|
||||
messageInput.disabled = true;
|
||||
messageInput.onkeydown = ({target}) => {
|
||||
if (target.value === 'enter') {
|
||||
sendMessage()
|
||||
}
|
||||
};
|
||||
|
||||
const actionButton = document.createElement('button');
|
||||
actionButton.innerText = 'Set your username!';
|
||||
|
||||
disposables.add(websocket.on('message', (message: any) => {
|
||||
if (message.pong) {
|
||||
console.log('[WebSocket] Received message', message);
|
||||
const rtt = message.pong.rtt || 0;
|
||||
|
||||
rttContainer.innerHTML = `RTT: [<span id="rtt-value">${rtt.toFixed(2)}</span>] ms`;
|
||||
} else {
|
||||
messagesElement.innerHTML += `<div class="message"> [${new Date(message.sentAt).toLocaleString("en-US", {timeStyle: 'short'})}]${message.message.author}: ${message.message.payload}</div>`;
|
||||
}
|
||||
}));
|
||||
disposables.add(websocket);
|
||||
|
||||
chatContainer.append(websocketStatusContainer);
|
||||
chatContainer.append(rttContainer);
|
||||
chatContainer.append(messagesElement);
|
||||
chatContainer.append(messageInput);
|
||||
chatContainer.append(actionButton);
|
||||
|
||||
appContainer.append(chatContainer);
|
||||
actionButton.onclick = () => {
|
||||
const usernamePrompt = prompt('Enter your name:');
|
||||
|
||||
if (!usernamePrompt) {
|
||||
return;
|
||||
}
|
||||
|
||||
username = usernamePrompt;
|
||||
messageInput.disabled = false;
|
||||
messageInput.placeholder = 'Enter a message...';
|
||||
actionButton.innerText = 'Send message!';
|
||||
actionButton.onclick = sendMessage;
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
console.log('[actionButton] Set username');
|
||||
|
||||
if (!messageInput.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
websocket.sendRequest('message', {
|
||||
author: username,
|
||||
recipient: 'chat',
|
||||
payload: messageInput.value
|
||||
}
|
||||
);
|
||||
|
||||
messageInput.value = ''
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
console.log('window load');
|
||||
const statusSubscription = websocket.status.subscribe(status => {
|
||||
websocketStatusContainer.innerHTML = `<h4 id="websocket-status">WebSocket Status [${WebSocketConnectionStatus[status]}]</h4>`;
|
||||
});
|
||||
|
||||
disposables.add(statusSubscription);
|
||||
|
||||
window.addEventListener('beforeunload', disposables.dispose.bind(disposables));
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
export class Message<T> {
|
||||
private readonly _author: string;
|
||||
private readonly _recipient: string;
|
||||
private readonly _payload: T
|
||||
|
||||
constructor(author, recipient, payload) {
|
||||
this._author = author;
|
||||
this._recipient = recipient;
|
||||
this._payload = payload;
|
||||
}
|
||||
|
||||
get author(): string {
|
||||
return this._author;
|
||||
}
|
||||
|
||||
get recipient(): string {
|
||||
return this._recipient;
|
||||
}
|
||||
|
||||
get payload(): T {
|
||||
return this._payload;
|
||||
}
|
||||
}
|
||||
|
||||
export type CreateMessageParameters<T> = {
|
||||
author: string;
|
||||
recipient: string;
|
||||
payload: T
|
||||
}
|
||||
|
||||
export default class MessageFactory {
|
||||
public static createMessage<T>({
|
||||
author,
|
||||
recipient,
|
||||
payload
|
||||
}: CreateMessageParameters<T>): Message<T> {
|
||||
|
||||
return new Message(author, recipient, payload);
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
throw new Error('MessageFactory is a static class that may not be instantiated');
|
||||
}
|
||||
}
|
||||
44
Web/WebSocket/websocket-chat/apps/frontend/src/styles.css
Normal file
44
Web/WebSocket/websocket-chat/apps/frontend/src/styles.css
Normal file
@@ -0,0 +1,44 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#websocket-status-container {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#rtt-container {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#chat-container {
|
||||
width: 500px;
|
||||
height: 300px;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#messages-container {
|
||||
width: 80%;
|
||||
height: 600px;
|
||||
margin: auto;
|
||||
border: 2px solid black;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#chat-container input, button {
|
||||
width: 79%;
|
||||
margin: auto;
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import {Subject, ReadOnlySubject, Disposable, DisposableList, EventPublisher} from '@techniker-me/tools';
|
||||
import type {Milliseconds} from '../../types/Units';
|
||||
import {WebSocketConnectionStatus} from './WebSocketConnectionStatus';
|
||||
|
||||
const pingIntervalDuration: Milliseconds = 2000;
|
||||
|
||||
export default class WebSocketConnection extends EventPublisher {
|
||||
private readonly _connectionDisposables = new DisposableList();
|
||||
private readonly _status: Subject<WebSocketConnectionStatus> = new Subject(WebSocketConnectionStatus.Closed);
|
||||
private readonly _readOnlyStatus: ReadOnlySubject<WebSocketConnectionStatus> = new ReadOnlySubject(this._status);
|
||||
private readonly _socket: WebSocket;
|
||||
|
||||
constructor(url: string) {
|
||||
super();
|
||||
this._socket = new WebSocket(url);
|
||||
|
||||
this.initialize(this._socket);
|
||||
}
|
||||
|
||||
get status(): ReadOnlySubject<WebSocketConnectionStatus> {
|
||||
return this._readOnlyStatus;
|
||||
}
|
||||
|
||||
public on(event: string, handler: (...args: unknown[]) => void): Disposable {
|
||||
return super.subscribe(event, handler);
|
||||
}
|
||||
|
||||
public sendRequest<T>(name: string, payload?: T) {
|
||||
try {
|
||||
const payloadStringified = JSON.stringify({
|
||||
sentAt: Date.now(),
|
||||
[name]: payload
|
||||
});
|
||||
|
||||
this._socket.send(payloadStringified);
|
||||
} catch (error){
|
||||
console.error('[WebSocket] Error sending request [%o]', error);
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._connectionDisposables.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private initialize(socket: WebSocket) {
|
||||
const websocketStart = performance.now();
|
||||
let pingIntervalId;
|
||||
|
||||
pingIntervalId = window.setInterval(() => {
|
||||
this.sendRequest('ping', {sentAt: Date.now()});
|
||||
}, pingIntervalDuration);
|
||||
|
||||
this.setStatus(WebSocketConnectionStatus.Connecting);
|
||||
|
||||
socket.onerror = error => {
|
||||
this.setStatus(WebSocketConnectionStatus.Error);
|
||||
window.clearInterval(pingIntervalId);
|
||||
console.error('[WebSocket] Error [%o]', error);
|
||||
};
|
||||
socket.onopen = () => {
|
||||
this.setStatus(WebSocketConnectionStatus.Open);
|
||||
|
||||
const websocketEnd = performance.now();
|
||||
|
||||
console.log(`[WebSocket] Connection time [${(websocketEnd - websocketStart)}] milliseconds`);
|
||||
};
|
||||
socket.onclose = () => {
|
||||
this.setStatus(WebSocketConnectionStatus.Closed);
|
||||
};
|
||||
socket.onmessage = (messageEvent) => {
|
||||
try {
|
||||
const messageData = JSON.parse(messageEvent.data);
|
||||
super.publish('message', messageData);
|
||||
} catch (error) {
|
||||
console.log('[WebSocket] Received non-JSON message [%s]', messageEvent.data);
|
||||
console.error('[WebSocket] Error parsing message [%o]', error);
|
||||
}
|
||||
};
|
||||
|
||||
this._connectionDisposables.add(new Disposable(() => {
|
||||
window.clearInterval(pingIntervalId);
|
||||
}));
|
||||
this._connectionDisposables.add(new Disposable(() => {
|
||||
socket.onerror = null;
|
||||
socket.onopen = null;
|
||||
socket.onclose = null;
|
||||
socket.onmessage = null;
|
||||
}));
|
||||
this._connectionDisposables.add(new Disposable(() => {
|
||||
// @ts-expect-error Disposing the subject
|
||||
this._status.value = null;
|
||||
}));
|
||||
this._connectionDisposables.add(new Disposable(socket.close.bind(socket)));
|
||||
|
||||
}
|
||||
|
||||
private setStatus(status: WebSocketConnectionStatus) {
|
||||
this._status.value = status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export enum WebSocketConnectionStatus {
|
||||
Connecting = 0,
|
||||
Open = 1,
|
||||
Closing = 2,
|
||||
Closed = 3,
|
||||
Error = 4
|
||||
}
|
||||
|
||||
30
Web/WebSocket/websocket-chat/apps/frontend/tsconfig.json
Normal file
30
Web/WebSocket/websocket-chat/apps/frontend/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "../../packages/shared-types" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export type Milliseconds = number;
|
||||
export type Seconds = number;
|
||||
@@ -0,0 +1,6 @@
|
||||
This favicon was generated using the following graphics from Twitter Twemoji:
|
||||
|
||||
- Graphics Title: 1f916.svg
|
||||
- Graphics Author: Copyright 2020 Twitter, Inc and other contributors (https://github.com/twitter/twemoji)
|
||||
- Graphics Source: https://github.com/twitter/twemoji/blob/master/assets/svg/1f916.svg
|
||||
- Graphics License: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 680 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{"src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png"},
|
||||
{"src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png"}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
15
Web/WebSocket/websocket-chat/apps/server/eslint.config.ts
Normal file
15
Web/WebSocket/websocket-chat/apps/server/eslint.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import js from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import json from '@eslint/json';
|
||||
import markdown from '@eslint/markdown';
|
||||
import css from '@eslint/css';
|
||||
import {defineConfig} from 'eslint/config';
|
||||
|
||||
export default defineConfig([
|
||||
{files: ['**/*.{js,mjs,cjs,ts,mts,cts}'], plugins: {js}, extends: ['js/recommended'], languageOptions: {globals: globals.node}},
|
||||
tseslint.configs.recommended,
|
||||
{files: ['**/*.json'], plugins: {json}, language: 'json/json', extends: ['json/recommended']},
|
||||
{files: ['**/*.md'], plugins: {markdown}, language: 'markdown/commonmark', extends: ['markdown/recommended']},
|
||||
{files: ['**/*.css'], plugins: {css}, language: 'css/css', extends: ['css/recommended']}
|
||||
]);
|
||||
61
Web/WebSocket/websocket-chat/apps/server/package.json
Normal file
61
Web/WebSocket/websocket-chat/apps/server/package.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "@techniker-me/websocket-server",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"prelint": "npm install",
|
||||
"lint": "eslint --max-warnings 0 .",
|
||||
"prelint:fix": "npm run format",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"dev": "tsx watch --clear-screen=false src/index.ts",
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"author": "Alexander Zinn",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@eslint/css": "0.11.1",
|
||||
"@eslint/js": "9.36.0",
|
||||
"@eslint/json": "0.13.2",
|
||||
"@eslint/markdown": "7.3.0",
|
||||
"@types/body-parser": "1.19.6",
|
||||
"@types/cors": "2.8.19",
|
||||
"@types/lru-cache": "7.10.9",
|
||||
"@types/morgan": "1.9.10",
|
||||
"@types/multer": "2.0.0",
|
||||
"@types/node": "24.5.2",
|
||||
"@types/on-headers": "1.0.4",
|
||||
"@types/response-time": "2.3.9",
|
||||
"@types/serve-favicon": "2.5.7",
|
||||
"@types/ws": "8.18.1",
|
||||
"eslint": "9.36.0",
|
||||
"globals": "16.4.0",
|
||||
"jiti": "2.6.0",
|
||||
"nodemon": "3.1.10",
|
||||
"prettier": "3.6.2",
|
||||
"tsx": "4.20.6",
|
||||
"typescript": "5.9.2",
|
||||
"typescript-eslint": "8.44.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@techniker-me/logger": "0.0.15",
|
||||
"@techniker-me/tools": "2025.0.16",
|
||||
"body-parser": "2.2.0",
|
||||
"cors": "2.8.5",
|
||||
"express": "5.1.0",
|
||||
"lru-cache": "11.2.2",
|
||||
"moment": "2.30.1",
|
||||
"morgan": "1.10.1",
|
||||
"multer": "2.0.2",
|
||||
"on-headers": "1.1.0",
|
||||
"permessage-deflate": "0.1.7",
|
||||
"response-time": "2.3.4",
|
||||
"serve-favicon": "2.5.1",
|
||||
"websocket-extensions": "0.1.4",
|
||||
"ws": "8.18.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type {HealthStatus} from './IHealthCheck';
|
||||
|
||||
export default class HealthCheck {
|
||||
public checkHealth(): HealthStatus {
|
||||
return {
|
||||
status: 'ok',
|
||||
environment: 'development',
|
||||
app: 'websocket-server',
|
||||
version: '0.0.1',
|
||||
zone: 'us-central1'
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import type {Request, Response} from 'express';
|
||||
import type IRoute from '../net/http/IRoutes';
|
||||
import IHealthCheck from './IHealthCheck';
|
||||
|
||||
export default class HealthCheckRoute implements IRoute {
|
||||
private readonly _healthCheck: IHealthCheck;
|
||||
|
||||
constructor(healthCheck: IHealthCheck) {
|
||||
this._healthCheck = healthCheck;
|
||||
}
|
||||
|
||||
public getGETRoutes() {
|
||||
console.log('[HealthCheckRoute] getGETRoutes called');
|
||||
const routes = {
|
||||
'/ok.html': (req, res) => this.externalReadiness.call(this, req, res),
|
||||
'/ping': (req, res) => this.ping.call(this, req, res)
|
||||
};
|
||||
console.log('[HealthCheckRoute] returning routes:', Object.keys(routes));
|
||||
return routes;
|
||||
}
|
||||
|
||||
public getPOSTRoutes() {
|
||||
return {};
|
||||
}
|
||||
|
||||
public getPUTRoutes() {
|
||||
return {};
|
||||
}
|
||||
|
||||
public getPATCHRoutes() {
|
||||
return {};
|
||||
}
|
||||
|
||||
public getDELETERoutes() {
|
||||
return {};
|
||||
}
|
||||
|
||||
private externalReadiness(_req: Request, res: Response) {
|
||||
console.log('[HealthCheckRoute] External readiness');
|
||||
res.setHeader('Cache-Control', 'public, max-age=0, no-cache, no-store');
|
||||
|
||||
try {
|
||||
const result = this._healthCheck.checkHealth();
|
||||
|
||||
if (!result || !result.status) {
|
||||
return res.status(500).end();
|
||||
}
|
||||
|
||||
switch (result.status) {
|
||||
case 'ok':
|
||||
case 'draining':
|
||||
case 'draining2':
|
||||
case 'disabled':
|
||||
return res.status(200).json(result);
|
||||
case 'starting':
|
||||
case 'drained':
|
||||
case 'stopped':
|
||||
return res.status(503).json(result);
|
||||
default:
|
||||
return res.status(500).json(result);
|
||||
}
|
||||
} catch {
|
||||
return res.status(500).end();
|
||||
}
|
||||
}
|
||||
|
||||
private ping(_req: Request, res: Response) {
|
||||
console.log('[HealthCheckRoute] Ping handler called from [%s]', _req.ip);
|
||||
|
||||
res.status(200).send({status: 'pong'}).end();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export type HealthStatus = {
|
||||
status: 'ok' | 'draining' | 'draining2' | 'disabled' | 'starting' | 'drained' | 'stopped';
|
||||
environment: string;
|
||||
app: string;
|
||||
version: string;
|
||||
zone: string;
|
||||
};
|
||||
|
||||
export default interface IHealthCheck {
|
||||
checkHealth(): HealthStatus;
|
||||
}
|
||||
78
Web/WebSocket/websocket-chat/apps/server/src/index.ts
Normal file
78
Web/WebSocket/websocket-chat/apps/server/src/index.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import path from 'node:path';
|
||||
import {IncomingMessage} from 'node:http';
|
||||
import {LoggerFactory} from '@techniker-me/logger';
|
||||
import HttpServer from './net/http/HttpServer';
|
||||
import HealthCheckRoute from './health/HealthCheckRoute';
|
||||
import HealthCheck from './health/HealthCheck';
|
||||
import WebSocketServer, {ExtendedWebSocket} from './net/websocket/WebSocketServer';
|
||||
|
||||
const logger = LoggerFactory.getLogger('Server');
|
||||
const healthCheck = new HealthCheck();
|
||||
const healthCheckRoute = new HealthCheckRoute(healthCheck);
|
||||
|
||||
const httpServer = new HttpServer('http', 3000, healthCheckRoute, {}, '', [], path.resolve(process.cwd(), 'assets', 'favicon', 'favicon.ico'), {});
|
||||
httpServer.on('error', () => logger.error('[HttpServer] Error'));
|
||||
|
||||
const sockets = new Map<string, ExtendedWebSocket>();
|
||||
|
||||
function connectDelegate(connection: ExtendedWebSocket, req: IncomingMessage) {
|
||||
console.log('[Server] Connect delegate');
|
||||
sockets.set(connection.id, connection);
|
||||
}
|
||||
|
||||
function requestDelegate(connection: ExtendedWebSocket, message: Buffer) {
|
||||
console.log('[Server] Request delegate');
|
||||
try {
|
||||
const messageJson = JSON.parse(message.toString());
|
||||
|
||||
console.log('messageJson', messageJson);
|
||||
|
||||
|
||||
if (messageJson.ping) {
|
||||
const serverTime = Date.now();
|
||||
const rtt = serverTime - messageJson.ping.sentAt;
|
||||
|
||||
connection.send(
|
||||
JSON.stringify({
|
||||
pong: {
|
||||
sentAt: serverTime,
|
||||
rtt: rtt
|
||||
},
|
||||
...messageJson
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
else if (messageJson.message) {
|
||||
sockets.forEach(socket => {
|
||||
socket.send(JSON.stringify({...messageJson}));
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('error requestingDelegate to handle websocket message', error);
|
||||
}
|
||||
}
|
||||
|
||||
function disconnectDelegate(connection: ExtendedWebSocket, reasonCode: number, description: string) {
|
||||
console.log('[Server] Disconnect delegate');
|
||||
}
|
||||
|
||||
function pongDelegate(connection: ExtendedWebSocket, message: Buffer) {
|
||||
console.log('[Server] Pong delegate');
|
||||
}
|
||||
|
||||
httpServer
|
||||
.start()
|
||||
.then(() => {
|
||||
const server = httpServer.getServer();
|
||||
|
||||
if (server) {
|
||||
const websocketServer = new WebSocketServer(server, {path: '/ws'});
|
||||
|
||||
websocketServer.start(connectDelegate, requestDelegate, disconnectDelegate, pongDelegate);
|
||||
} else {
|
||||
console.error('[Server] Failed to get HTTP server instance');
|
||||
}
|
||||
})
|
||||
.catch(err => console.log('[Server] Server failed to start:', err));
|
||||
170
Web/WebSocket/websocket-chat/apps/server/src/lang/Assert.ts
Normal file
170
Web/WebSocket/websocket-chat/apps/server/src/lang/Assert.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
export default class Assert {
|
||||
public static isUndefined(name: string, value: unknown): asserts value is undefined {
|
||||
if (value !== undefined) {
|
||||
throw new Error(`[${name}] must be undefined instead received [${typeof value}]`);
|
||||
}
|
||||
}
|
||||
|
||||
public static isNull(name: string, value: unknown): asserts value is null {
|
||||
if (value !== null) {
|
||||
throw new Error(`[${name}] must be null instead received [${typeof value}]`);
|
||||
}
|
||||
}
|
||||
|
||||
public static isDefined(name: string, value: unknown): asserts value is NonNullable<typeof value> {
|
||||
if (value === undefined || value === null) {
|
||||
throw new Error(`[${name}] must be defined instead received [${typeof value}]`);
|
||||
}
|
||||
}
|
||||
|
||||
public static isBoolean(name: string, value: unknown): asserts value is boolean {
|
||||
if (!Assert._isBoolean(value)) {
|
||||
throw new Error(`[${name}] must be a boolean instead received [${typeof value}]`);
|
||||
}
|
||||
}
|
||||
|
||||
public static isTrue(name: string, value: unknown): asserts value is true {
|
||||
Assert.isBoolean(name, value);
|
||||
|
||||
if (!value) {
|
||||
throw new Error(`[${name}] must be true`);
|
||||
}
|
||||
}
|
||||
|
||||
public static isString(name: string, value: unknown): asserts value is string {
|
||||
if (!Assert._isString(value)) {
|
||||
throw new Error(`[${name}] must be a string instead received [${typeof value}]`);
|
||||
}
|
||||
}
|
||||
|
||||
public static isNonEmptyString(name: string, value: unknown): asserts value is string {
|
||||
if (!Assert._isNonEmptyString(value)) {
|
||||
throw new Error(`[${name}] must be a non-empty string instead received [${typeof value}]`);
|
||||
}
|
||||
}
|
||||
|
||||
public static isNumber(name: string, value: unknown): asserts value is number {
|
||||
if (!Assert._isNumber(value)) {
|
||||
throw new Error(`[${name}] must be a number instead received [${typeof value}]`);
|
||||
}
|
||||
}
|
||||
|
||||
public static isInteger(name: string, value: unknown): asserts value is number {
|
||||
Assert.isNumber(name, value);
|
||||
|
||||
if (!Number.isInteger(value)) {
|
||||
throw new Error(`[${name}] must be an integer, received [${value}]`);
|
||||
}
|
||||
}
|
||||
|
||||
public static isPositiveNumber(name: string, value: unknown): asserts value is number {
|
||||
if (!Assert._isNumberPositive(value)) {
|
||||
throw new Error(`[${name}] must be a positive number instead received [${typeof value}]`);
|
||||
}
|
||||
}
|
||||
|
||||
public static isNumberInRange(name: string, lowerBound: number, upperBound: number, value: unknown): asserts value is number {
|
||||
if (upperBound < lowerBound) {
|
||||
throw new Error(`Invalid Range: [${name}] bounds are invalid, lower bound [${lowerBound}] must be less than upper bound [${upperBound}]`);
|
||||
}
|
||||
|
||||
if (!(Assert._isNumber(value) && Assert._isNumberInRange(value, lowerBound, upperBound))) {
|
||||
throw new Error(`[${name}] must have a value between [${lowerBound}] and [${upperBound}] instead received [${value}]`);
|
||||
}
|
||||
}
|
||||
|
||||
public static isFunction(name: string, value: unknown): asserts value is (...args: unknown[]) => unknown {
|
||||
if (typeof value !== 'function') {
|
||||
throw new Error(`[${name}] must be a function, instead received [${typeof value}]`);
|
||||
}
|
||||
}
|
||||
|
||||
public static satisfiesInterface<T>(name: string, obj: unknown, requiredProps: (keyof T)[]): asserts obj is T {
|
||||
Assert.isObject(name, obj);
|
||||
|
||||
for (const prop of requiredProps) {
|
||||
if (!(prop in (obj as Record<string, unknown>))) {
|
||||
throw new Error(`[${name}] missing required property: ${String(prop)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static isArray(name: string, value: unknown): asserts value is unknown[] {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error(`[${name}] must be an array, instead received [${typeof value}]`);
|
||||
}
|
||||
}
|
||||
|
||||
public static isArrayOf<T>(name: string, arrayValueType: T, value: unknown): asserts value is T[] {
|
||||
Assert.isArray(name, value);
|
||||
|
||||
for (const item of value) {
|
||||
const itemTypeof = typeof item;
|
||||
|
||||
if (itemTypeof !== arrayValueType) {
|
||||
throw new Error(`[${name}] must be an array of [${arrayValueType}] received [${itemTypeof}]`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static isStringArray(name: string, value: unknown): asserts value is string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error(`[${name}] must be an array, instead received [${typeof value}]`);
|
||||
}
|
||||
|
||||
for (const item of value) {
|
||||
Assert.isString(name, item);
|
||||
}
|
||||
}
|
||||
|
||||
public static isObject<T>(name: string, value: unknown): asserts value is T {
|
||||
if (value === null || typeof value !== 'object') {
|
||||
throw new Error(`[${name}] must be an object, instead received [${typeof value}]`);
|
||||
}
|
||||
}
|
||||
|
||||
public static isEnumMember<T extends object>(name: string, enumObj: T, value: unknown): asserts value is T[keyof T] {
|
||||
if (!Object.values(enumObj).includes(value as T[keyof T])) {
|
||||
throw new Error(`[${name}] is not a member of the enum`);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
public static isInstance<T>(name: string, parentClass: new (...args: any[]) => T, object: unknown): asserts object is T {
|
||||
if (object === null || object === undefined || typeof object !== 'object') {
|
||||
throw new Error(`[${name}] must be an instance of [${parentClass.constructor.name}], instead received [${typeof object}]`);
|
||||
}
|
||||
|
||||
if (!(object instanceof parentClass)) {
|
||||
throw new Error(`[${name}] must be an instance of [${parentClass.constructor.name}], instead received [${object.constructor.name}]`);
|
||||
}
|
||||
}
|
||||
|
||||
private static _isBoolean(value: unknown): value is boolean {
|
||||
return typeof value === 'boolean';
|
||||
}
|
||||
|
||||
private static _isString(value: unknown): value is string {
|
||||
return typeof value === 'string';
|
||||
}
|
||||
|
||||
private static _isNonEmptyString(value: unknown): value is string {
|
||||
return Assert._isString(value) && value.length > 0;
|
||||
}
|
||||
|
||||
private static _isNumber(value: unknown): value is number {
|
||||
return typeof value === 'number';
|
||||
}
|
||||
|
||||
private static _isNumberPositive(value: unknown): value is number {
|
||||
return Assert._isNumber(value) && value > 0;
|
||||
}
|
||||
|
||||
private static _isNumberInRange(value: unknown, lowerBound: number, upperBound: number): value is number {
|
||||
return Assert._isNumber(value) && value >= lowerBound && value <= upperBound;
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
throw new Error('Assert is a static class that may not be instantiated');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export default class Strings {
|
||||
public static randomString(length: number): string {
|
||||
return (Date.now().toString(36) + Math.random().toString(36).substring(2)).substring(0, length);
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
throw new Error('Strings is a static class that may not be instantiated');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
import {Nullable} from '../../types/optional';
|
||||
import Assert from '../../lang/Assert';
|
||||
import moment from 'moment';
|
||||
import type {Server} from 'node:http';
|
||||
import http from 'node:http';
|
||||
import {EventEmitter} from 'node:events';
|
||||
import type {ILogger} from '@techniker-me/logger';
|
||||
import {LoggerFactory} from '@techniker-me/logger';
|
||||
import IRoutes from './IRoutes';
|
||||
import {Subject} from '@techniker-me/tools';
|
||||
import express, {RequestHandler} from 'express';
|
||||
import morgan from 'morgan';
|
||||
import favicon from 'serve-favicon';
|
||||
import bodyParser from 'body-parser';
|
||||
import multer from 'multer';
|
||||
import {Kilobytes} from '../../types/Units';
|
||||
import responseTime from 'response-time';
|
||||
import onHeaders from 'on-headers';
|
||||
import {LRUCache} from 'lru-cache';
|
||||
|
||||
const requestSizeLimit: Kilobytes = 10240;
|
||||
const defaultTcpSocketTimeout = moment.duration(720, 'seconds'); // Google HTTPS load balancer expects at least 600 seconds
|
||||
const defaultKeepAliveTimeout = moment.duration(660, 'seconds'); // Google HTTPS load balancer expects at least 600 seconds
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age#:~:text=The%20Access%2DControl%2DMax%2D,Headers%20headers)%20can%20be%20cached.
|
||||
const corsAccessControlMaxAge = moment.duration(24, 'hours');
|
||||
const shortTermCaching = 'public, max-age=20, s-maxage=20';
|
||||
const tlsSessionTimeout = moment.duration(5, 'minutes');
|
||||
const maxCachedTlsSessions = 1000;
|
||||
|
||||
export default class HttpServer {
|
||||
private readonly _logger: ILogger = LoggerFactory.getLogger('HttpServer');
|
||||
private readonly _eventEmitter: EventEmitter;
|
||||
private readonly _protocol: 'http' | 'https';
|
||||
private readonly _port: number;
|
||||
private readonly _routes: IRoutes;
|
||||
// @ts-expect-error - unused parameter for future functionality
|
||||
private readonly _viewsPath: object;
|
||||
// @ts-expect-error - unused parameter for future functionality
|
||||
private readonly _viewParameters: string;
|
||||
private readonly _resourcesPaths: string[];
|
||||
private readonly _favicon: string;
|
||||
private readonly _cors: object;
|
||||
private readonly _app: Subject<Nullable<express.Application>>;
|
||||
private readonly _server: Subject<Nullable<Server>>;
|
||||
private readonly _tlsSessionCache = new LRUCache({
|
||||
ttl: tlsSessionTimeout.asMilliseconds(),
|
||||
max: maxCachedTlsSessions
|
||||
});
|
||||
private _jsonHandler: Nullable<express.RequestHandler>;
|
||||
|
||||
constructor(
|
||||
protocol: 'https' | 'http',
|
||||
port: number,
|
||||
routes: IRoutes,
|
||||
viewsPath: object,
|
||||
viewParameters: string,
|
||||
resourcesPaths: string[],
|
||||
favicon: string,
|
||||
cors: object
|
||||
) {
|
||||
Assert.isString('protocol', protocol);
|
||||
Assert.isNumber('port', port);
|
||||
Assert.satisfiesInterface('routes', routes, ['getGETRoutes', 'getPOSTRoutes', 'getPUTRoutes', 'getPATCHRoutes', 'getDELETERoutes']);
|
||||
// Assert.isObjectOf<string>('viewsPath', viewsPath);
|
||||
Assert.isString('viewParameters', viewParameters);
|
||||
Assert.isArrayOf<string>('resourcesPaths', 'string', resourcesPaths);
|
||||
Assert.isString('favicon', favicon);
|
||||
// Assert.isObjectOf<string>('cors', cors, 'string');
|
||||
|
||||
this._protocol = protocol;
|
||||
this._port = port;
|
||||
this._routes = routes;
|
||||
this._viewsPath = viewsPath;
|
||||
this._viewParameters = viewParameters;
|
||||
this._resourcesPaths = resourcesPaths;
|
||||
this._favicon = favicon;
|
||||
this._cors = cors;
|
||||
this._app = new Subject<Nullable<express.Application>>(null);
|
||||
this._eventEmitter = new EventEmitter();
|
||||
this._server = new Subject<Nullable<Server>>(null);
|
||||
this._jsonHandler = null;
|
||||
}
|
||||
|
||||
public start() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const app = (this._app.value = express());
|
||||
this._jsonHandler = bodyParser.json({limit: requestSizeLimit});
|
||||
|
||||
this.configureListener();
|
||||
this.configureMiddleware();
|
||||
this.configureResources();
|
||||
this.configureRoutes();
|
||||
|
||||
app.set('x-powered-by', false);
|
||||
|
||||
const server = (this._server.value = http.createServer(app));
|
||||
|
||||
const onListen = () => {
|
||||
this._logger.info('HTTP Server listening on %s://*:%s', this._protocol, this._port);
|
||||
|
||||
server.removeListener('error', onError);
|
||||
|
||||
resolve(this);
|
||||
};
|
||||
|
||||
const onError = (err: unknown) => {
|
||||
server.removeListener('listening', onListen);
|
||||
|
||||
reject(err);
|
||||
};
|
||||
|
||||
server.keepAliveTimeout = defaultKeepAliveTimeout.milliseconds();
|
||||
server.timeout = defaultTcpSocketTimeout.asMilliseconds();
|
||||
server.setTimeout(defaultTcpSocketTimeout.asMilliseconds());
|
||||
server.once('error', onError);
|
||||
server.once('listening', onListen);
|
||||
|
||||
server.on('request', (req, res) => {
|
||||
this._eventEmitter.emit('request', req.method, req.url, res.statusCode, req.headers);
|
||||
});
|
||||
|
||||
server.on('newSession', (sessionId, sessionData, callback) => {
|
||||
const cacheId = sessionId.toString('hex');
|
||||
|
||||
this._tlsSessionCache.set(cacheId, sessionData);
|
||||
this._logger.debug('Created new TLS session [%s]', cacheId);
|
||||
|
||||
callback();
|
||||
});
|
||||
|
||||
server.on('resumeSession', (sessionId, callback) => {
|
||||
const cacheId = sessionId.toString('hex');
|
||||
const sessionData = this._tlsSessionCache.get(cacheId);
|
||||
|
||||
callback(null, sessionData);
|
||||
|
||||
if (sessionData) {
|
||||
this._logger.debug('Resumed TLS session [%s]', cacheId);
|
||||
} else {
|
||||
this._logger.debug('TLS session [%s] not found', cacheId);
|
||||
}
|
||||
});
|
||||
|
||||
server.listen({
|
||||
port: this._port,
|
||||
backlog: 16 * 1024
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public on(event: string, handler: (...args: unknown[]) => void): void {
|
||||
Assert.isNonEmptyString('event', event);
|
||||
Assert.isFunction('handler', handler);
|
||||
|
||||
this._eventEmitter.on(event, handler);
|
||||
}
|
||||
|
||||
public getServer(): Nullable<Server> {
|
||||
return this._server.value;
|
||||
}
|
||||
|
||||
private configureListener() {
|
||||
if (!this._app.value) {
|
||||
throw new Error('Unable to configure listener, no app instance found');
|
||||
}
|
||||
|
||||
const app = this._app.value;
|
||||
|
||||
app.use((req, res, next) => {
|
||||
req.on('finish', () => {
|
||||
this._eventEmitter.emit('request', req.method, req.url, res.statusCode, req.headers);
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
private configureMiddleware() {
|
||||
if (!this._app.value) {
|
||||
throw new Error('Unable to configure middleware, no app instance found');
|
||||
}
|
||||
|
||||
const app = this._app.value;
|
||||
const logger = this._logger;
|
||||
|
||||
app.use(morgan('common'));
|
||||
app.enable('trust proxy');
|
||||
app.set('env', 'development'); // setting env to test prevents logging to the console
|
||||
app.use(favicon(this._favicon));
|
||||
app.use(
|
||||
morgan(
|
||||
':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" :response-time',
|
||||
{
|
||||
stream: {
|
||||
write(line) {
|
||||
logger.info(line.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
app.use(
|
||||
bodyParser.text({
|
||||
type: 'application/sdp',
|
||||
limit: requestSizeLimit
|
||||
})
|
||||
);
|
||||
app.use(
|
||||
bodyParser.text({
|
||||
type: 'application/trickle-ice-sdpfrag',
|
||||
limit: requestSizeLimit
|
||||
})
|
||||
);
|
||||
app.use(
|
||||
bodyParser.urlencoded({
|
||||
extended: true,
|
||||
limit: requestSizeLimit
|
||||
})
|
||||
);
|
||||
app.use(
|
||||
multer({
|
||||
limits: {
|
||||
fields: 1,
|
||||
fieldNameSize: 100,
|
||||
fieldSize: requestSizeLimit,
|
||||
files: 0,
|
||||
parts: 1,
|
||||
headerPairs: 1
|
||||
}
|
||||
}).none()
|
||||
);
|
||||
app.use((req, _res, next) => {
|
||||
const contentType = req?.headers?.['content-type'] || '';
|
||||
|
||||
if (contentType.startsWith('multipart/form-data;')) {
|
||||
if (req?.body?.jsonBody) {
|
||||
req.body = JSON.parse(req.body.jsonBody);
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
app.use(responseTime());
|
||||
app.use((req, res, next) => {
|
||||
res.set('x-origination', 'Platform');
|
||||
|
||||
if (req.secure) {
|
||||
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
if (this._cors) {
|
||||
this._logger.info('Enable CORS on %s://*:%s', this._protocol, this._port);
|
||||
|
||||
const cachedCorsAllowOrigins: Record<string, string> = {};
|
||||
const getCorsAllowOrigin = (url: string) => {
|
||||
let corsAllowOrigin = cachedCorsAllowOrigins[url];
|
||||
|
||||
if (!Object.hasOwn(cachedCorsAllowOrigins, url)) {
|
||||
Object.entries(this._cors).forEach(([key, value]) => {
|
||||
if (url.startsWith(key)) {
|
||||
corsAllowOrigin = value;
|
||||
}
|
||||
});
|
||||
|
||||
cachedCorsAllowOrigins[url] = corsAllowOrigin ?? '';
|
||||
}
|
||||
|
||||
return corsAllowOrigin;
|
||||
};
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const corsAllowOrigin = getCorsAllowOrigin(req.url);
|
||||
|
||||
if (corsAllowOrigin) {
|
||||
res.header('Access-Control-Allow-Origin', corsAllowOrigin);
|
||||
res.header(
|
||||
'Access-Control-Allow-Headers',
|
||||
'Authorization, Origin, Range, X-Requested-With, If-Modified-Since, Accept, Keep-Alive, Cache-Control, Content-Type, DNT'
|
||||
);
|
||||
res.header('Access-Control-Allow-Methods', 'POST, GET, HEAD, OPTIONS, PUT, PATCH, DELETE');
|
||||
res.header('Access-Control-Expose-Headers', 'Server, Range, Date, Content-Disposition, X-Timer, ETag, Link, Location');
|
||||
res.header('Access-Control-Max-Age', corsAccessControlMaxAge.asSeconds().toString());
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.header('Cache-Control', shortTermCaching);
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
app.use((_req, res, next) => {
|
||||
const startTimeSeconds = Date.now() / 1000;
|
||||
const startTimeNanoseconds = process.hrtime.bigint();
|
||||
|
||||
onHeaders(res, () => {
|
||||
const durationNanoseconds = process.hrtime.bigint() - startTimeNanoseconds;
|
||||
const durationMilliseconds = durationNanoseconds / 1000000n;
|
||||
|
||||
// https://developer.fastly.com/reference/http/http-headers/X-Timer/
|
||||
// S{unixStartTimeSeconds},VS0,VE{durationMilliseconds}
|
||||
res.setHeader('X-Timer', `S${startTimeSeconds},VS0,VE${durationMilliseconds}`);
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
private configureResources() {
|
||||
if (!this._app.value) {
|
||||
throw new Error('Unable to configure resources, no app instance found');
|
||||
}
|
||||
|
||||
const app = this._app.value;
|
||||
|
||||
for (const resourcePath of this._resourcesPaths) {
|
||||
app.use(express.static(resourcePath));
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private errorHandler(err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) {
|
||||
this._logger.error(err.message);
|
||||
|
||||
res.status(500).send({status: 'error'});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private genericNotFoundHandler(_req: express.Request, res: express.Response) {
|
||||
console.log('Generic not found handler');
|
||||
|
||||
res.status(404).send({status: 'not-found'});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private configureRoutes() {
|
||||
if (!this._app.value) {
|
||||
throw new Error('Unable to configure routes, no app instance found');
|
||||
}
|
||||
|
||||
const app = this._app.value;
|
||||
|
||||
let catchAllHandler: Nullable<RequestHandler> = null;
|
||||
|
||||
const registerRoutes = (method: string, routes: Record<string, RequestHandler>): void => {
|
||||
for (const route of Object.entries(routes)) {
|
||||
if (!route) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [name, handler] = route;
|
||||
|
||||
if (name === '*') {
|
||||
if (catchAllHandler) {
|
||||
throw new Error(`Only one catch-all handler can ber registered per server, ignoring conflicting catch-all`);
|
||||
}
|
||||
|
||||
catchAllHandler = handler;
|
||||
continue;
|
||||
}
|
||||
|
||||
this._logger.debug(`Registering [${method}] route [${name}] handler`);
|
||||
app[method.toLowerCase() as 'get' | 'post' | 'put' | 'patch' | 'delete'](name, handler);
|
||||
}
|
||||
};
|
||||
|
||||
registerRoutes('GET', this._routes.getGETRoutes());
|
||||
registerRoutes('POST', this._routes.getPOSTRoutes());
|
||||
registerRoutes('PUT', this._routes.getPUTRoutes());
|
||||
registerRoutes('PATCH', this._routes.getPATCHRoutes());
|
||||
registerRoutes('DELETE', this._routes.getDELETERoutes());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type {RequestHandler} from 'express';
|
||||
|
||||
export default interface IRoutes {
|
||||
getGETRoutes(): Record<string, RequestHandler>;
|
||||
getPOSTRoutes(): Record<string, RequestHandler>;
|
||||
getPUTRoutes(): Record<string, RequestHandler>;
|
||||
getPATCHRoutes(): Record<string, RequestHandler>;
|
||||
getDELETERoutes(): Record<string, RequestHandler>;
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import {LoggerFactory} from '@techniker-me/logger';
|
||||
import {Server as HttpServer} from 'node:http';
|
||||
import {IncomingMessage} from 'node:http';
|
||||
import ws, {WebSocketServer as WSServer, WebSocket} from 'ws';
|
||||
import Assert from '../../lang/Assert';
|
||||
import Strings from '../../lang/Strings';
|
||||
import WebsocketExtensions from 'websocket-extensions';
|
||||
|
||||
export interface ExtendedWebSocket extends WebSocket {
|
||||
id: string;
|
||||
remoteAddress: string;
|
||||
isOpen(): boolean;
|
||||
isClosed(): boolean;
|
||||
}
|
||||
|
||||
function deepClone<T>(obj: T): T {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (obj instanceof Date) {
|
||||
return new Date(obj.getTime()) as T;
|
||||
}
|
||||
|
||||
if (obj instanceof Array) {
|
||||
return obj.map(item => deepClone(item)) as T;
|
||||
}
|
||||
|
||||
if (obj instanceof Object) {
|
||||
const copy = {} as T;
|
||||
Object.keys(obj).forEach(key => {
|
||||
copy[key as keyof T] = deepClone(obj[key as keyof T]);
|
||||
});
|
||||
return copy;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function getRemoteAddress(connection: ExtendedWebSocket, req: IncomingMessage): string {
|
||||
// WebSocket doesn't expose socket directly, so we need to use the underlying socket
|
||||
const socket = (connection as WebSocket & {_socket?: {remoteAddress?: string}})._socket;
|
||||
const remoteAddress = socket?.remoteAddress || '::';
|
||||
const xForwardedFor = req.headers['x-forwarded-for'];
|
||||
|
||||
let forwardingRemoteAddressChain: string[] = [];
|
||||
if (typeof xForwardedFor === 'string') {
|
||||
forwardingRemoteAddressChain = xForwardedFor.split(', ').filter(Boolean);
|
||||
} else if (Array.isArray(xForwardedFor)) {
|
||||
forwardingRemoteAddressChain = xForwardedFor.flatMap(addr => addr.split(', ')).filter(Boolean);
|
||||
}
|
||||
|
||||
forwardingRemoteAddressChain.push(remoteAddress);
|
||||
|
||||
// For now, just return the first address (most direct)
|
||||
// TODO: Implement proper proxy trust checking
|
||||
return forwardingRemoteAddressChain[forwardingRemoteAddressChain.length - 1] || '::';
|
||||
}
|
||||
|
||||
export type WebSovketServerOptions = {
|
||||
path?: string;
|
||||
};
|
||||
|
||||
const connectionIdLength = 32;
|
||||
|
||||
export default class WebSocketServer {
|
||||
private readonly _logger = LoggerFactory.getLogger('WebSocketServer');
|
||||
private readonly _httpServer: HttpServer;
|
||||
private readonly _parameters: Record<string, string>;
|
||||
private _server?: WSServer;
|
||||
private _extensions?: WebsocketExtensions;
|
||||
|
||||
constructor(httpServer: HttpServer, parameters: WebSovketServerOptions) {
|
||||
this._httpServer = httpServer;
|
||||
this._parameters = parameters;
|
||||
}
|
||||
|
||||
public start(
|
||||
connectDelegate: (connection: ExtendedWebSocket, req: IncomingMessage) => void,
|
||||
requestDelegate: (connection: ExtendedWebSocket, message: Buffer) => void,
|
||||
disconnectDelegate: (connection: ExtendedWebSocket, reasonCode: number, description: string) => void,
|
||||
pongDelegate: (connection: ExtendedWebSocket, message: Buffer) => void
|
||||
): void {
|
||||
Assert.isFunction('connectDelegate', connectDelegate);
|
||||
Assert.isFunction('requestDelegate', requestDelegate);
|
||||
Assert.isFunction('disconnectDelegate', disconnectDelegate);
|
||||
Assert.isFunction('pongDelegate', pongDelegate);
|
||||
|
||||
const serverOptions = deepClone(this._parameters);
|
||||
|
||||
const address = this._httpServer.address();
|
||||
const port = typeof address === 'string' ? address : address?.port?.toString() || 'unknown';
|
||||
const path = serverOptions['path'] || '/';
|
||||
|
||||
this._logger.info(`Listening on port [${port}] and bound to [${path}]`);
|
||||
|
||||
(serverOptions as Record<string, unknown>)['noServer'] = true;
|
||||
|
||||
this._server = new WSServer(serverOptions);
|
||||
this._extensions = new WebsocketExtensions();
|
||||
|
||||
// this._extensions.add(deflate());
|
||||
(this._server as WSServer & {_server?: HttpServer})._server = this._httpServer;
|
||||
this._httpServer.on('error', this._server.emit.bind(this._server, 'error'));
|
||||
this._httpServer.on('listening', this._server.emit.bind(this._server, 'listening'));
|
||||
this._httpServer.on('upgrade', (req, socket, head) => {
|
||||
this._logger.debug(`[HttpServer] Upgrade to WebSocket: ${req.method} ${req.url}`);
|
||||
if (!req.url?.startsWith(path)) {
|
||||
this._logger.debug(`Skipping upgrade of http request due to incorrect path [${req.url}]`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._server!.handleUpgrade(req, socket, head, (ws: WebSocket) => {
|
||||
this._server!.emit('connection', ws, req as IncomingMessage);
|
||||
});
|
||||
});
|
||||
this._httpServer.on('request', (req, res) => {
|
||||
this._logger.debug(`[HttpServer] Request: ${req.method} ${req.url} -> ${res.statusCode}`);
|
||||
});
|
||||
|
||||
this._server.on('error', err => this._logger.error('An error occurred with WebSocket', err));
|
||||
this._server.on('connection', (connection: WebSocket, req: IncomingMessage) => {
|
||||
let closed = false;
|
||||
|
||||
try {
|
||||
const extendedConnection = connection as ExtendedWebSocket;
|
||||
extendedConnection.id = Strings.randomString(connectionIdLength);
|
||||
extendedConnection.remoteAddress = getRemoteAddress(extendedConnection, req);
|
||||
extendedConnection.isOpen = () => connection.readyState === ws.OPEN;
|
||||
extendedConnection.isClosed = () => connection.readyState === ws.CLOSED;
|
||||
|
||||
connection.on('error', (e: Error) => {
|
||||
this._logger.error('An error occurred on websocket', e);
|
||||
});
|
||||
|
||||
connection.on('message', (message: Buffer) => {
|
||||
try {
|
||||
requestDelegate(extendedConnection, message);
|
||||
} catch (e) {
|
||||
this._logger.error('Request handler failed for message [%s]', message, e);
|
||||
}
|
||||
});
|
||||
|
||||
connection.on('close', (reasonCode: number, description: string) => {
|
||||
if (closed) {
|
||||
this._logger.warn('[%s] Multiple close events [%s] [%s] [%s]', extendedConnection.id, extendedConnection.remoteAddress, reasonCode, description);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
closed = true;
|
||||
|
||||
try {
|
||||
disconnectDelegate(extendedConnection, reasonCode, description);
|
||||
} catch (e) {
|
||||
this._logger.error('Disconnect handler failed', e);
|
||||
}
|
||||
});
|
||||
|
||||
connection.on('pong', (message: Buffer) => {
|
||||
try {
|
||||
pongDelegate(extendedConnection, message);
|
||||
} catch (e) {
|
||||
this._logger.error('Pong handler failed', e);
|
||||
}
|
||||
});
|
||||
|
||||
connectDelegate(extendedConnection, req);
|
||||
} catch (e) {
|
||||
this._logger.error('Accept/connect handler failed', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
||||
18
Web/WebSocket/websocket-chat/apps/server/src/types/Units.ts
Normal file
18
Web/WebSocket/websocket-chat/apps/server/src/types/Units.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export type Bytes = number;
|
||||
export type Kilobytes = number;
|
||||
export type Megabytes = number;
|
||||
export type Gigabytes = number;
|
||||
export type Terabytes = number;
|
||||
export type Petabytes = number;
|
||||
export type Exabytes = number;
|
||||
export type Zettabytes = number;
|
||||
export type Yottabytes = number;
|
||||
|
||||
export type Milliseconds = number;
|
||||
export type Seconds = number;
|
||||
export type Minutes = number;
|
||||
export type Hours = number;
|
||||
export type Days = number;
|
||||
export type Weeks = number;
|
||||
export type Months = number;
|
||||
export type Years = number;
|
||||
32
Web/WebSocket/websocket-chat/apps/server/src/types/modules.d.ts
vendored
Normal file
32
Web/WebSocket/websocket-chat/apps/server/src/types/modules.d.ts
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
declare module 'websocket-extensions' {
|
||||
interface Extension {
|
||||
name: string;
|
||||
params?: Record<string, string | number | boolean>;
|
||||
}
|
||||
|
||||
class WebSocketExtensions {
|
||||
constructor();
|
||||
add(extension: {name: string; [key: string]: unknown}): void;
|
||||
negotiate(protocol: string, extensions: string[]): string | false;
|
||||
parse(extensions: string): Extension[];
|
||||
generate(extensions: Extension[]): string;
|
||||
}
|
||||
|
||||
export = WebSocketExtensions;
|
||||
}
|
||||
|
||||
declare module 'permessage-deflate' {
|
||||
interface DeflateOptions {
|
||||
serverNoContextTakeover?: boolean;
|
||||
clientNoContextTakeover?: boolean;
|
||||
serverMaxWindowBits?: number;
|
||||
clientMaxWindowBits?: number;
|
||||
level?: number;
|
||||
memLevel?: number;
|
||||
strategy?: number;
|
||||
}
|
||||
|
||||
function deflate(options?: DeflateOptions): {name: string; [key: string]: unknown};
|
||||
|
||||
export = deflate;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export type Optional<T> = T | undefined | null;
|
||||
export type Nullable<T> = T | null;
|
||||
30
Web/WebSocket/websocket-chat/apps/server/tsconfig.json
Normal file
30
Web/WebSocket/websocket-chat/apps/server/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"lib": ["es2022"],
|
||||
"moduleResolution": "node",
|
||||
"module": "Preserve",
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": false,
|
||||
"isolatedModules": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noEmit": false,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"composite": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"references": [
|
||||
{ "path": "../../packages/shared-types" }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user