Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rpc): init network package #485

Merged
merged 2 commits into from
Nov 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/network/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# @univerjs/network

## Introduction

This plugin provides network services to other modules of Univer.

## Usage

### Installation

```shell
npm i @univerjs/network
```

### API

Check [Univer](https://github.com/dream-num/univer/)
11 changes: 11 additions & 0 deletions packages/network/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "@univerjs/network",
"private": true,
"main": "./src/index.ts",
"module": "./src/index.ts",
"dependencies": {
"@univerjs/core": "workspace:*",
"@wendellhu/redi": "^0.12.10",
"rxjs": "^7.8.1"
}
}
6 changes: 6 additions & 0 deletions packages/network/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { HTTPHeaders } from './services/http/headers';
export { HTTPService } from './services/http/http.service';
export { XHRHTTPImplementation } from './services/http/implementations/xhr';
export { HTTPRequest } from './services/http/request';
export { HTTPResponse } from './services/http/response';
export { type ISocket, ISocketService, WebSocketService } from './services/web-socket/web-socket.service';
43 changes: 43 additions & 0 deletions packages/network/src/services/http/headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
interface IHeadersConstructorProps {
[key: string]: string | number | boolean;
}

/**
* It wraps headers of HTTP requests' and responses' headers.
*/
export class HTTPHeaders {
private readonly _headers: { [key: string]: string[] } = {};

constructor(headers?: IHeadersConstructorProps | string) {
if (typeof headers === 'string') {
// split header text and serialize them to HTTPHeaders
headers.split('\n').forEach((header) => {
const [name, value] = header.split(':');
if (name && value) {
this._setHeader(name, value);
}
});
} else {
if (headers) {
Object.keys(headers).forEach(([name, value]) => {
this._setHeader(name, value);
});
}
}
}

forEach(callback: (name: string, value: string[]) => void): void {
Object.keys(this._headers).forEach((name) => {
callback(name, this._headers[name]);
});
}

private _setHeader(name: string, value: string | number | boolean): void {
const lowerCase = name.toLowerCase();
if (this._headers[lowerCase]) {
this._headers[lowerCase].push(value.toString());
} else {
this._headers[lowerCase] = [value.toString()];
}
}
}
130 changes: 130 additions & 0 deletions packages/network/src/services/http/http.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { Disposable, Nullable, remove, toDisposable } from '@univerjs/core';
import { IDisposable } from '@wendellhu/redi';
import { firstValueFrom, Observable, of } from 'rxjs';
import { concatMap } from 'rxjs/operators';

import { HTTPHeaders } from './headers';
import { HTTPResponseType } from './http';
import { IHTTPImplementation } from './implementations/implementation';
import { HTTPParams } from './params';
import { HTTPRequest, HTTPRequestMethod } from './request';
import { HTTPEvent, HTTPResponse, HTTPResponseError } from './response';

// TODO: error handling of HTTPService should be strengthened.

export interface IRequestParams {
/** Query params. These params would be append to the url before the request is sent. */
params?: { [param: string]: string | number | boolean };
headers?: { [key: string]: string | number | boolean };
responseType?: HTTPResponseType;
withCredentials?: boolean;
}

export interface IPostRequestParams extends IRequestParams {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
body?: any;
}

type HTTPHandlerFn = (request: HTTPRequest) => Observable<HTTPEvent<unknown>>;
type HTTPInterceptorFn = (request: HTTPRequest, next: HTTPHandlerFn) => Observable<HTTPEvent<unknown>>;
type RequestPipe<T> = (req: HTTPRequest, finalHandlerFn: HTTPHandlerFn) => Observable<HTTPEvent<T>>;

/**
* Register an HTTP interceptor. Interceptor could modify requests, responses, headers or errors.
*/
export interface IHTTPInterceptor {
priority?: number;
interceptor: HTTPInterceptorFn;
}

export class HTTPService extends Disposable {
private _interceptors: IHTTPInterceptor[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private _pipe: Nullable<RequestPipe<any>>;

constructor(@IHTTPImplementation private readonly _http: IHTTPImplementation) {
super();
}

registerHTTPInterceptor(interceptor: IHTTPInterceptor): IDisposable {
if (this._interceptors.indexOf(interceptor) !== -1) {
throw new Error('[HTTPService]: The interceptor has already been registered!');
}

this._interceptors.push(interceptor);
this._interceptors = this._interceptors.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));

this._pipe = null;

return toDisposable(() => remove(this._interceptors, interceptor));
}

get<T>(url: string, options?: IRequestParams): Promise<HTTPResponse<T>> {
return this._request<T>('GET', url, options);
}

post<T>(url: string, options?: IPostRequestParams): Promise<HTTPResponse<T>> {
return this._request<T>('POST', url, options);
}

put<T>(url: string, options?: IPostRequestParams): Promise<HTTPResponse<T>> {
return this._request<T>('PUT', url, options);
}

delete<T>(url: string, options?: IRequestParams): Promise<HTTPResponse<T>> {
return this._request<T>('DELETE', url, options);
}

/** The HTTP request implementations */
private async _request<T>(
method: HTTPRequestMethod,
url: string,
options?: IRequestParams
): Promise<HTTPResponse<T>> {
// Things to do when sending a HTTP request:
// 1. Generate HTTPRequest/HTTPHeader object
// 2. Call interceptors and finally the HTTP implementation.
const headers = new HTTPHeaders(options?.headers);
const params = new HTTPParams(options?.params);
const request = new HTTPRequest(method, url, {
headers,
params,
withCredentials: options?.withCredentials ?? false, // default value for withCredentials is false by MDN
responseType: options?.responseType ?? 'json',
});

const events$: Observable<HTTPEvent<any>> = of(request).pipe(
concatMap((request) => this._runInterceptorsAndImplementation(request))
);

// The event$ may emit multiple values, but we only care about the first one.
// We may need to care about other events (especially progress events) in the future.
const result = await firstValueFrom(events$);
if (result instanceof HTTPResponse) {
return result;
}

throw new Error(`${(result as HTTPResponseError).error}`);
}

private _runInterceptorsAndImplementation(request: HTTPRequest): Observable<HTTPEvent<any>> {
// In this method we first call all interceptors and finally the HTTP implementation.
// And the HTTP response will be passed back through the interceptor chain.
if (!this._pipe) {
this._pipe = this._interceptors
.map((handler) => handler.interceptor)
.reduceRight(
(nextHandlerFunction, interceptorFunction: HTTPInterceptorFn) =>
chainInterceptorFn(nextHandlerFunction, interceptorFunction),
(requestFromPrevInterceptor, finalHandler) => finalHandler(requestFromPrevInterceptor)
);
}

return this._pipe!(request, (requestToNext) => this._http.send(requestToNext) /* final handler */);
}
}

function chainInterceptorFn(afterInterceptorChain: HTTPInterceptorFn, currentInterceptorFn: HTTPInterceptorFn) {
return (prevRequest: HTTPRequest, nextHandlerFn: HTTPHandlerFn) =>
currentInterceptorFn(prevRequest, (nextRequest) => afterInterceptorChain(nextRequest, nextHandlerFn));
}
82 changes: 82 additions & 0 deletions packages/network/src/services/http/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* eslint-disable no-magic-numbers */

export type HTTPResponseType = 'arraybuffer' | 'blob' | 'json' | 'text';

export const SuccessStatusCodeLowerBound = 200;

export const ErrorStatusCodeLowerBound = 300;

/**
* Http status codes.
*
* https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
*/
export enum HTTPStatusCode {
Continue = 100,
SwitchingProtocols = 101,
Processing = 102,
EarlyHints = 103,

Ok = 200,
Created = 201,
Accepted = 202,
NonAuthoritativeInformation = 203,
NoContent = 204,
ResetContent = 205,
PartialContent = 206,
MultiStatus = 207,
AlreadyReported = 208,
ImUsed = 226,

MultipleChoices = 300,
MovedPermanently = 301,
Found = 302,
SeeOther = 303,
NotModified = 304,
UseProxy = 305,
Unused = 306,
TemporaryRedirect = 307,
PermanentRedirect = 308,

BadRequest = 400,
Unauthorized = 401,
PaymentRequired = 402,
Forbidden = 403,
NotFound = 404,
MethodNotAllowed = 405,
NotAcceptable = 406,
ProxyAuthenticationRequired = 407,
RequestTimeout = 408,
Conflict = 409,
Gone = 410,
LengthRequired = 411,
PreconditionFailed = 412,
PayloadTooLarge = 413,
UriTooLong = 414,
UnsupportedMediaType = 415,
RangeNotSatisfiable = 416,
ExpectationFailed = 417,
ImATeapot = 418,
MisdirectedRequest = 421,
UnprocessableEntity = 422,
Locked = 423,
FailedDependency = 424,
TooEarly = 425,
UpgradeRequired = 426,
PreconditionRequired = 428,
TooManyRequests = 429,
RequestHeaderFieldsTooLarge = 431,
UnavailableForLegalReasons = 451,

InternalServerError = 500,
NotImplemented = 501,
BadGateway = 502,
ServiceUnavailable = 503,
GatewayTimeout = 504,
HttpVersionNotSupported = 505,
VariantAlsoNegotiates = 506,
InsufficientStorage = 507,
LoopDetected = 508,
NotExtended = 510,
NetworkAuthenticationRequired = 511,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// TODO
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { createIdentifier } from '@wendellhu/redi';
import { Observable } from 'rxjs';

import { HTTPRequest } from '../request';
import { HTTPEvent } from '../response';

/**
* HTTP service could be implemented differently on platforms.
*/
export interface IHTTPImplementation {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// There may be stream response so the return value is an observable.
send(request: HTTPRequest): Observable<HTTPEvent<any>>;
}
export const IHTTPImplementation = createIdentifier<IHTTPImplementation>('univer-pro.network.http-implementation');
Loading