Skip to content

Commit

Permalink
feat(server-context): server/api request base classes (#1219)
Browse files Browse the repository at this point in the history
  • Loading branch information
alimd authored Jun 8, 2023
2 parents cdbdce6 + e2f6814 commit d7a25f9
Show file tree
Hide file tree
Showing 14 changed files with 350 additions and 3 deletions.
4 changes: 2 additions & 2 deletions core/fsm2/src/fsm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ export abstract class FiniteStateMachine<S extends string, E extends string> ext
}

/**
* Subscribe to context changes.
* Subscribe to state changes.
*/
subscribe(listenerCallback: ListenerCallback<this, S>, options?: SubscribeOptions): SubscribeResult {
return super._subscribe(listenerCallback, options);
}

/**
* Unsubscribe from context.
* Unsubscribe from changes.
*/
unsubscribe(listenerCallback: ListenerCallback<this, S>): void {
return super._unsubscribe(listenerCallback);
Expand Down
Empty file.
3 changes: 3 additions & 0 deletions core/server-context/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Alwatr Server Context - `@alwatr/server-context`

Elegant powerful server-context manager base on alwatr signal, written in tiny TypeScript, ES module.
44 changes: 44 additions & 0 deletions core/server-context/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@alwatr/server-context",
"version": "0.32.0",
"description": "Elegant powerful context manager base on alwatr signal, written in tiny TypeScript, ES module.",
"keywords": [
"context",
"server-context",
"signal",
"typescript",
"esm",
"alwatr"
],
"main": "index.js",
"type": "module",
"types": "index.d.ts",
"author": "S. Ali Mihandoost <ali.mihandoost@gmail.com> (https://ali.mihandoost.com)",
"license": "MIT",
"files": [
"**/*.{d.ts.map,d.ts,js.map,js,html,md}"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/AliMD/alwatr",
"directory": "core/server-context"
},
"homepage": "https://github.com/AliMD/alwatr/tree/main/core/server-context#readme",
"bugs": {
"url": "https://github.com/AliMD/alwatr/issues"
},
"dependencies": {
"@alwatr/fetch": "^0.32.0",
"@alwatr/fsm": "^0.32.0",
"@alwatr/logger": "^0.32.0",
"@alwatr/signal": "^0.32.0",
"@alwatr/util": "^0.32.0",
"tslib": "^2.5.2"
},
"devDependencies": {
"@alwatr/type": "^0.32.0"
}
}
103 changes: 103 additions & 0 deletions core/server-context/src/api-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {NODE_MODE} from '@alwatr/logger';
import {getClientId} from '@alwatr/util';

import {AlwatrServerRequestBase} from './server-request.js';

import type {ServerRequestState} from './server-request.js';
import type {FetchOptions} from '@alwatr/fetch/type.js';
import type {ListenerCallback, SubscribeOptions, SubscribeResult} from '@alwatr/signal2';
import type {AlwatrServiceResponse, Stringifyable, StringifyableRecord} from '@alwatr/type';

export abstract class AlwatrApiRequestBase<
TData extends Stringifyable = Stringifyable,
TMeta extends StringifyableRecord = StringifyableRecord
> extends AlwatrServerRequestBase {
protected _responseJson?: AlwatrServiceResponse<TData, TMeta>;

protected override async _$fetch(options: FetchOptions): Promise<void> {
if (!NODE_MODE) {
options.headers ??= {};
if (!options.headers['client-id']) {
options.headers['client-id'] = getClientId();
}
}

await super._$fetch(options);

let responseText: string;
try {
responseText = await this._response!.text();
}
catch (err) {
this._logger.error('_$fetch', 'invalid_response_text', err);
throw err;
}

try {
this._responseJson = JSON.parse(responseText);
}
catch (err) {
this._logger.error('_$fetch', 'invalid_response_json', err, {responseText});
throw err;
}

const responseJson = this._responseJson!;
if (responseJson.ok !== true) {
if (typeof responseJson.errorCode === 'string') {
this._logger.accident('_$fetch', responseJson.errorCode, 'fetch response not ok', {responseJson});
throw new Error(responseJson.errorCode);
}
else {
this._logger.error('_$fetch', 'fetch_nok', 'fetch response json not ok', {responseJson});
throw new Error('fetch_nok');
}
}
}

protected override _reset(): void {
super._reset();
delete this._responseJson;
}
}

export class AlwatrApiRequest<
TData extends Stringifyable = Stringifyable,
TMeta extends StringifyableRecord = StringifyableRecord
> extends AlwatrApiRequestBase<TData, TMeta> {
/**
* Current state.
*/
get state(): ServerRequestState {
return this._state;
}

get response(): AlwatrServiceResponse<TData, TMeta> | undefined {
return this._responseJson;
}

get _fetchResponse(): Response | undefined {
return this._response;
}

request(options?: Partial<FetchOptions>): void {
return this._request(options);
}

/**
* Subscribe to state changes.
*/
subscribe(listenerCallback: ListenerCallback<this, ServerRequestState>, options?: SubscribeOptions): SubscribeResult {
return this._subscribe(listenerCallback, options);
}

/**
* Unsubscribe from changes.
*/
unsubscribe(listenerCallback: ListenerCallback<this, ServerRequestState>): void {
return super._unsubscribe(listenerCallback);
}

cleanup(): void {
this._reset();
}
}
2 changes: 2 additions & 0 deletions core/server-context/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './server-request.js';
export * from './api-request.js';
134 changes: 134 additions & 0 deletions core/server-context/src/server-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import {fetch} from '@alwatr/fetch';
import {FiniteStateMachineBase} from '@alwatr/fsm2';

import type {FetchOptions} from '@alwatr/fetch/type.js';
import type {ListenerCallback, SubscribeOptions, SubscribeResult} from '@alwatr/signal2';

export interface ServerRequestConfig extends Partial<FetchOptions> {
name: string;
}

export type ServerRequestState = 'initial' | 'loading' | 'failed' | 'complete';

export type ServerRequestEvent = 'request' | 'requestFailed' | 'requestSuccess';

export abstract class AlwatrServerRequestBase extends FiniteStateMachineBase<ServerRequestState, ServerRequestEvent> {
protected _$fetchOptions?: FetchOptions;
protected _response?: Response;

constructor(protected _config: ServerRequestConfig) {
super({name: _config.name, initialState: 'initial'});

this._stateRecord = {
initial: {
request: 'loading',
},
loading: {
requestFailed: 'failed',
requestSuccess: 'complete',
},
failed: {
request: 'loading',
},
complete: {
request: 'loading',
},
};

this._actionRecord = {
_on_loading_enter: this._requestAction,
};
}

protected _request(options?: Partial<FetchOptions>): void {
this._logger.logMethodArgs?.('_request', options);
this._setOptions(options);
this._transition('request');
}

protected async _$fetch(options: FetchOptions): Promise<void> {
this._logger.logMethodArgs?.('_$fetch', options);
this._response = await fetch(options);

if (!this._response.ok) {
throw new Error('fetch_nok');
}
}

protected async _requestAction(): Promise<void> {
this._logger.logMethod?.('_requestAction');

try {
if (this._$fetchOptions === undefined) {
throw new Error('invalid_fetch_options');
}

await this._$fetch(this._$fetchOptions);

this._transition('requestSuccess');
}
catch (err) {
this._logger.error('_request', 'fetch_failed', err);
this._transition('requestFailed');
}
}

protected _setOptions(options?: Partial<FetchOptions>): void {
this._logger.logMethodArgs?.('_setOptions', {options});

const fetchOptions = {
...this._config,
...options,
queryParameters: {
...this._config.queryParameters,
...options?.queryParameters,
},
};

if (fetchOptions.url == null) {
throw new Error('invalid_fetch_options');
}

this._$fetchOptions = fetchOptions as FetchOptions;
}

protected override _reset(): void {
super._reset();
delete this._response;
}
}

export class AlwatrServerRequest extends AlwatrServerRequestBase {
/**
* Current state.
*/
get state(): ServerRequestState {
return this._state;
}

get response(): Response | undefined {
return this._response;
}

request(options?: Partial<FetchOptions>): void {
return this._request(options);
}

/**
* Subscribe to state changes.
*/
subscribe(listenerCallback: ListenerCallback<this, ServerRequestState>, options?: SubscribeOptions): SubscribeResult {
return this._subscribe(listenerCallback, options);
}

/**
* Unsubscribe from changes.
*/
unsubscribe(listenerCallback: ListenerCallback<this, ServerRequestState>): void {
return this._unsubscribe(listenerCallback);
}

cleanup(): void {
this._reset();
}
}
20 changes: 20 additions & 0 deletions core/server-context/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"extends": "../../tsconfig.base",
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": ".tsbuildinfo",
"rootDir": "src",
"outDir": "."
},

"include": ["src/**/*.ts"],
"exclude": [],
"references": [
{"path": "../logger"},
{"path": "../signal2"},
{"path": "../util"},
{"path": "../fetch"},
{"path": "../fsm2"},
{"path": "../type"}
]
}
1 change: 1 addition & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<li><a href="./es-bench/">ES Bench</a></li>
<li><a href="./finite-state-machine/">FSM</a></li>
<li><a href="./validator/">Validator</a></li>
<li><a href="./server-context/">Server Context</a></li>
</ol>
</body>
</html>
13 changes: 13 additions & 0 deletions demo/server-context/api-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {AlwatrApiRequest} from '@alwatr/server-context';

const apiRequest = new AlwatrApiRequest({
name: 'demo.server-request',
});

apiRequest.request({
url: 'https://order.soffit.co/api/v1/publistore/',
});

apiRequest.subscribe(() => {
console.log('serverRequest: %o', {state: apiRequest.state, response: apiRequest.response});
});
13 changes: 13 additions & 0 deletions demo/server-context/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Server Context</title>
<script type="module" src="./server-context.js"></script>
</head>
<body>

</body>
</html>
13 changes: 13 additions & 0 deletions demo/server-context/server-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {AlwatrServerRequest} from '@alwatr/server-context';

const serverRequest = new AlwatrServerRequest({
name: 'demo.server-request',
});

serverRequest.request({
url: 'http://httpbin.org/uuid',
});

serverRequest.subscribe(async () => {
console.log('serverRequest: %o', {state: serverRequest.state, response: await serverRequest.response?.text()});
});
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
{"path": "./core/signal"},
{"path": "./core/signal2"},
{"path": "./core/context"},
{"path": "./core/server-context"},
{"path": "./core/router"},
{"path": "./core/i18n"},
{"path": "./core/math"},
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8443,7 +8443,7 @@ tslib@^1.8.1:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==

tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.5.0, tslib@^2.5.3:
tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.5.0, tslib@^2.5.2, tslib@^2.5.3:
version "2.5.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913"
integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==
Expand Down

0 comments on commit d7a25f9

Please sign in to comment.