Minor: Refactor the code-base for better organization and compartmentalization. (#934)
* Clean up unused code * Fix media player formatting issue for labels with new line characteres. * Refactor the media player handlers into a class. * More code cleanup and organize shared weather utils into distinct classes. * Flatten some nesting. * Move weather manager in dedicated class and build HTTP Utility class for Rest API calling. * Remove logs * Rebase master merge * Reorg code (WIP) * More reorg * Delete utility scripts * Reorg options * Finish moving all options over * Fix typescript issues * Update options imports to default * missed update * Screw barrel files honestly, work of the devil. * Only initialize power profiles if power-profiles-daemon is running. * Fix window positioning and weather service naming * style dir * More organization * Restructure types to be closer to their source * Remove lib types and constants * Update basic weather object to be saner with extensibility. * Service updates * Fix initialization strategy for services. * Fix Config Manager to only emit changed objects and added missing temp converters. * Update storage service to handle unit changes. * Added cpu temp sensor auto-discovery * Added missing JSDocs to services * remove unused * Migrate to network service. * Fix network password issue. * Move out password input into helper * Rename password mask constant to be less double-negativey. * Dropdown menu rename * Added a component to edit JSON in the settings dialog (rough/WIP) * Align settings * Add and style JSON Editor. * Adjust padding * perf(shortcuts): ⚡ avoid unnecessary polling when shortcuts are disabled Stops the recording poller when shortcuts are disabled, preventing redundant polling and reducing resource usage. * Fix types and return value if shortcut not enabled. * Move the swww daemon checking process outside of the wallpaper service into a dedicated deamon lifecyle processor. * Add more string formatters and use title case for weather status (as it was). * Fix startup errors. * Rgba fix * Remove zod from dependencies --------- Co-authored-by: KernelDiego <gonzalezdiego.contact@gmail.com>
This commit is contained in:
268
src/lib/httpClient/index.ts
Normal file
268
src/lib/httpClient/index.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { GLib } from 'astal';
|
||||
import Soup from 'gi://Soup?version=3.0';
|
||||
import { HttpError } from './HttpError';
|
||||
import { RequestOptions, RestResponse } from './types';
|
||||
import { errorHandler } from 'src/core/errors/handler';
|
||||
|
||||
/**
|
||||
* HTTP client wrapper for Soup.Session providing a Promise-based API
|
||||
* Handles authentication, timeouts, and JSON parsing automatically
|
||||
*/
|
||||
class HttpClient {
|
||||
private _session: Soup.Session;
|
||||
constructor(defaultTimeout = 30) {
|
||||
this._session = new Soup.Session();
|
||||
this._session.timeout = defaultTimeout;
|
||||
this._session.user_agent = 'HyprPanel/1.0';
|
||||
}
|
||||
|
||||
/*******************************************
|
||||
* HTTP Methods *
|
||||
*******************************************/
|
||||
|
||||
/**
|
||||
* Performs an HTTP GET request
|
||||
* @param url - Target URL for the request
|
||||
* @param options - Optional configuration for the request
|
||||
*/
|
||||
public async get(url: string, options?: RequestOptions): Promise<RestResponse> {
|
||||
return this._request('GET', url, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an HTTP POST request
|
||||
* @param url - Target URL for the request
|
||||
* @param data - Request payload to send
|
||||
* @param options - Optional configuration for the request
|
||||
*/
|
||||
public async post(
|
||||
url: string,
|
||||
data?: Record<string, unknown>,
|
||||
options?: RequestOptions,
|
||||
): Promise<RestResponse> {
|
||||
return this._request('POST', url, { ...options, body: data });
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an HTTP PUT request
|
||||
* @param url - Target URL for the request
|
||||
* @param data - Request payload to send
|
||||
* @param options - Optional configuration for the request
|
||||
*/
|
||||
public async put(
|
||||
url: string,
|
||||
data?: Record<string, unknown>,
|
||||
options?: RequestOptions,
|
||||
): Promise<RestResponse> {
|
||||
return this._request('PUT', url, { ...options, body: data });
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an HTTP PATCH request
|
||||
* @param url - Target URL for the request
|
||||
* @param data - Request payload with partial updates
|
||||
* @param options - Optional configuration for the request
|
||||
*/
|
||||
public async patch(
|
||||
url: string,
|
||||
data?: Record<string, unknown>,
|
||||
options?: RequestOptions,
|
||||
): Promise<RestResponse> {
|
||||
return this._request('PATCH', url, { ...options, body: data });
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an HTTP DELETE request
|
||||
* @param url - Target URL for the request
|
||||
* @param options - Optional configuration for the request
|
||||
*/
|
||||
public async delete(url: string, options?: RequestOptions): Promise<RestResponse> {
|
||||
return this._request('DELETE', url, options);
|
||||
}
|
||||
|
||||
/*******************************************
|
||||
* SOUP Infrastructure *
|
||||
*******************************************/
|
||||
|
||||
/**
|
||||
* Internal request handler for all HTTP methods
|
||||
* @param method - HTTP method to use
|
||||
* @param url - Target URL for the request
|
||||
* @param options - Configuration options for the request
|
||||
* @private
|
||||
*/
|
||||
private async _request(method: string, url: string, options: RequestOptions = {}): Promise<RestResponse> {
|
||||
const requestPromise = new Promise<RestResponse>((resolve, reject) => {
|
||||
const message = Soup.Message.new(method, url);
|
||||
|
||||
if (!message) {
|
||||
return reject(new Error(`Failed to create request for ${url}`));
|
||||
}
|
||||
|
||||
this._assignHeaders(message, options);
|
||||
this._constructBodyIfExists(method, options, message);
|
||||
|
||||
if (options.timeout) {
|
||||
this._session.timeout = options.timeout / 1000;
|
||||
}
|
||||
|
||||
this._sendRequest(resolve, reject, message, options);
|
||||
});
|
||||
|
||||
return requestPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs and sets the request body for HTTP methods that support it
|
||||
* @param method - HTTP method being used
|
||||
* @param options - Request options containing the body
|
||||
* @param message - Soup message to attach the body to
|
||||
*/
|
||||
private _constructBodyIfExists(method: string, options: RequestOptions, message: Soup.Message): void {
|
||||
const canContainBody = ['POST', 'PUT', 'PATCH'].includes(method);
|
||||
if (options.body && canContainBody) {
|
||||
let body: string;
|
||||
let contentType = options.headers?.['Content-Type'] || 'application/json';
|
||||
|
||||
if (typeof options.body === 'object') {
|
||||
body = JSON.stringify(options.body);
|
||||
} else {
|
||||
body = options.body;
|
||||
contentType = contentType || 'text/plain';
|
||||
}
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
const bytes = new GLib.Bytes(textEncoder.encode(body));
|
||||
message.set_request_body_from_bytes(contentType, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns headers to the request message
|
||||
* @param message - Soup message to add headers to
|
||||
* @param options - Request options containing headers
|
||||
*/
|
||||
private _assignHeaders(message: Soup.Message, options: RequestOptions): Soup.MessageHeaders {
|
||||
const headers = message.get_request_headers();
|
||||
|
||||
if (options.headers) {
|
||||
Object.entries(options.headers).forEach(([key, value]) => {
|
||||
headers.append(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the HTTP request and handles the response
|
||||
* @param resolve - Promise resolve callback
|
||||
* @param reject - Promise reject callback
|
||||
* @param message - Prepared Soup message to send
|
||||
* @param options - Request configuration options
|
||||
*/
|
||||
private _sendRequest(
|
||||
resolve: (value: RestResponse | PromiseLike<RestResponse>) => void,
|
||||
reject: (reason?: unknown) => void,
|
||||
message: Soup.Message,
|
||||
options: RequestOptions,
|
||||
): void {
|
||||
const cancellable = options.signal ?? null;
|
||||
|
||||
try {
|
||||
const bytes = this._session.send_and_read(message, cancellable);
|
||||
|
||||
const {
|
||||
response: responseText,
|
||||
headers: responseHeaders,
|
||||
status,
|
||||
} = this._decodeResponseSync(message, bytes);
|
||||
|
||||
const responseData = this._parseReponseData(options, responseText);
|
||||
|
||||
const response: RestResponse = {
|
||||
data: responseData,
|
||||
status,
|
||||
headers: responseHeaders,
|
||||
};
|
||||
|
||||
if (status >= 400) {
|
||||
const httpError = new HttpError({
|
||||
status,
|
||||
data: responseData,
|
||||
url: message.get_uri().to_string(),
|
||||
method: message.get_method(),
|
||||
});
|
||||
|
||||
return reject(httpError);
|
||||
}
|
||||
|
||||
return resolve(response);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the response bytes into text and extracts response metadata
|
||||
* @param message - Soup message containing the response
|
||||
* @param bytes - Response bytes from the sync request
|
||||
*/
|
||||
private _decodeResponseSync(
|
||||
message: Soup.Message,
|
||||
bytes: GLib.Bytes | null,
|
||||
): {
|
||||
response: string;
|
||||
status: Soup.Status;
|
||||
headers: Record<string, string>;
|
||||
} {
|
||||
if (!bytes) {
|
||||
throw new Error('No response received');
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
const byteData = bytes.get_data();
|
||||
|
||||
const responseText = byteData ? decoder.decode(byteData) : '';
|
||||
const status = message.get_status();
|
||||
|
||||
const responseHeaders: Record<string, string> = {};
|
||||
|
||||
message.get_response_headers().foreach((name, value) => {
|
||||
responseHeaders[name] = value;
|
||||
});
|
||||
|
||||
return {
|
||||
response: responseText,
|
||||
status,
|
||||
headers: responseHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses response text based on the expected response type
|
||||
* @param options - Request options containing responseType preference
|
||||
* @param responseText - Raw response text to parse
|
||||
*/
|
||||
private _parseReponseData(
|
||||
options: RequestOptions,
|
||||
responseText: string,
|
||||
): string | Record<string, unknown> {
|
||||
if (!responseText) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (options.responseType === 'text') {
|
||||
return responseText;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedResponseText = JSON.parse(responseText);
|
||||
return parsedResponseText;
|
||||
} catch (e) {
|
||||
errorHandler(`Failed to parse JSON response: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const httpClient = new HttpClient();
|
||||
Reference in New Issue
Block a user