Skip to content

Commit

Permalink
feat(rpc): init network package (#485)
Browse files Browse the repository at this point in the history
* feat(rpc): init network package

* chore: update pnpm lock
  • Loading branch information
wzhudev committed Nov 18, 2023
1 parent e990a4e commit 5a024f1
Show file tree
Hide file tree
Showing 14 changed files with 649 additions and 0 deletions.
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

0 comments on commit 5a024f1

Please sign in to comment.