-
-
Notifications
You must be signed in to change notification settings - Fork 503
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(rpc): init network package (#485)
* feat(rpc): init network package * chore: update pnpm lock
- Loading branch information
Showing
14 changed files
with
649 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()]; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
// TODO |
16 changes: 16 additions & 0 deletions
16
packages/network/src/services/http/implementations/implementation.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); |
Oops, something went wrong.