Skip to content

Commit

Permalink
feat(context): awesome server context
Browse files Browse the repository at this point in the history
Co-authored-by: Mohammad Honarvar <honarvar.info@gmail.com>
  • Loading branch information
alimd and mohammadhonarvar committed Mar 18, 2023
1 parent 2a1b152 commit c001c58
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 2 deletions.
5 changes: 3 additions & 2 deletions core/context/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
"description": "Elegant powerful context manager base on alwatr signal, written in tiny TypeScript, ES module.",
"keywords": [
"context",
"signal",
"typescript",
"esm",
"alwatr"
],
"main": "context.js",
"main": "index.js",
"type": "module",
"types": "context.d.ts",
"types": "index.d.ts",
"author": "S. Ali Mihandoost <ali.mihandoost@gmail.com> (https://ali.mihandoost.com)",
"license": "MIT",
"files": [
Expand Down
184 changes: 184 additions & 0 deletions core/context/src/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import {serviceRequest, type StringifyableFetchOptions} from '@alwatr/fetch';
import {finiteStateMachineProvider, type FsmConsumerInterface} from '@alwatr/fsm';
import {createLogger, globalAlwatr} from '@alwatr/logger';

import type {RequestServiceFsm, RequestServiceFsmContext} from './type.js';
import type {AlwatrServiceResponseSuccessWithMeta} from '@alwatr/type';

export const logger = createLogger('alwatr/context');

globalAlwatr.registeredList.push({
name: '@alwatr/context',
version: _ALWATR_VERSION_,
});

export const requestServiceFsmConstructor = finiteStateMachineProvider.defineConstructor('request_service_fsm', {
initial: 'initial',
context: <RequestServiceFsmContext>{},
stateRecord: {
$all: {
on: {},
},
initial: {
on: {
request_service: {
target: 'offlineLoading',
},
},
},
/**
* Just check offline cache data before online request.
*/
offlineLoading: {
entry: ['offline_mode', 'request'],
on: {
request_failed: {
target: 'loadingFailed',
},
request_success: {
target: 'reloading',
},
cache_not_found: {
target: 'onlineLoading',
},
},
},
/**
* First loading without any cached context.
*/
onlineLoading: {
entry: ['online_mode', 'request'],
on: {
request_failed: {
target: 'loadingFailed',
},
request_success: {
target: 'complete',
},
},
},
/**
* First loading failed without any cached context.
*/
loadingFailed: {
on: {
request_service: {
target: 'offlineLoading',
},
},
},
reloading: {
entry: ['online_mode', 'request'],
on: {
request_failed: {
target: 'reloadingFailed',
},
request_success: {
target: 'complete',
},
},
},
/**
* Reloading failed with previously cached context exist.
*/
reloadingFailed: {
on: {
request_service: {
target: 'reloading',
},
},
},
complete: {
on: {
request_service: {
target: 'reloading',
},
},
},
},
});

finiteStateMachineProvider.defineActions<RequestServiceFsm>('request_service_fsm', {
offline_mode: (fsmInstance) => {
logger.logMethod('action_offline_mode');
const {fetchOptions} = fsmInstance.getContext();
if (fetchOptions == null) return logger.error('action_offline_mode', 'invalid_fetch_options', {id: fsmInstance.id});
fetchOptions.cacheStrategy = 'cache_only';
},

online_mode: (fsmInstance) => {
logger.logMethod('action_online_mode');
const {fetchOptions} = fsmInstance.getContext();
if (fetchOptions == null) return logger.error('action_online_mode', 'invalid_fetch_options', {id: fsmInstance.id});
fetchOptions.cacheStrategy = 'update_cache';
},

request: async (fsmInstance) => {
logger.logMethod('action_request');

try {
const {response, fetchOptions} = fsmInstance.getContext();
if (fetchOptions == null) return logger.error('action_request', 'invalid_fetch_options', {id: fsmInstance.id});
const newResponse = await serviceRequest<NonNullable<RequestServiceFsmContext['response']>>(
fetchOptions as StringifyableFetchOptions,
);

if (
response != null &&
newResponse.meta?.lastUpdated != null &&
newResponse.meta.lastUpdated === response.meta?.lastUpdated
) {
// no changed
fsmInstance.transition('request_success');
}
else {
fsmInstance.transition('request_success', {response: newResponse});
}
}
catch (err) {
if ((err as Error).message === 'fetch_cache_not_found') {
fsmInstance.transition('cache_not_found');
}
else {
logger.error('requestOrderStorageContext', 'fetch_failed', err);
fsmInstance.transition('request_failed');
}
}
},
});

export const request = (
fsm: FsmConsumerInterface<RequestServiceFsm>,
options?: Partial<StringifyableFetchOptions>,
mergeOption = true,
): void => {
logger.logMethodArgs('request', fsm.id);
if (options != null) setOptions(fsm, options, mergeOption);
fsm.transition('request_service');
};

export const setOptions = (
fsm: FsmConsumerInterface<RequestServiceFsm>,
options: Partial<StringifyableFetchOptions>,
merge = true,
): void => {
logger.logMethodArgs('setOptions', fsm.id);
const {fetchOptions} = fsm.getContext();
// prettier-ignore
fsm.setContext({
fetchOptions: merge === false ? options : {
...options,
queryParameters: {
...fetchOptions?.queryParameters,
...options.queryParameters,
},
},
});
};

export const getResponse = <TResponse extends AlwatrServiceResponseSuccessWithMeta>(
fsm: FsmConsumerInterface<RequestServiceFsm, RequestServiceFsmContext<TResponse>>,
): TResponse | undefined => {
logger.logMethodArgs('getResponse', fsm.id);
return fsm.getContext().response;
};
46 changes: 46 additions & 0 deletions core/context/src/server-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {type StringifyableFetchOptions} from '@alwatr/fetch';
import {finiteStateMachineConsumer, type FsmConsumerInterface} from '@alwatr/fsm';

import {getResponse, request, setOptions} from './core.js';

import type {RequestServiceFsm, RequestServiceFsmContext} from './type.js';
import type {AlwatrDocumentStorage, AlwatrServiceResponseSuccessWithMeta, OmitFirstParam} from '@alwatr/type';
import type {Order} from '@alwatr/type/customer-order-management.js';

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export const serverContextConsumer = <TResponse extends AlwatrServiceResponseSuccessWithMeta = AlwatrDocumentStorage>(
instanceId: string,
options?: StringifyableFetchOptions,
) => {
const fsm = finiteStateMachineConsumer<RequestServiceFsm, RequestServiceFsmContext<TResponse>>(
instanceId,
'request_service_fsm',
);

if (options != null) {
setOptions(fsm as FsmConsumerInterface<RequestServiceFsm>, options);
}

return {
id: instanceId,
request: request.bind(null, fsm as FsmConsumerInterface<RequestServiceFsm>) as OmitFirstParam<typeof request>,
setOptions: setOptions.bind(null, fsm as FsmConsumerInterface<RequestServiceFsm>) as OmitFirstParam<
typeof setOptions
>,
getResponse: getResponse.bind(null, fsm as FsmConsumerInterface<RequestServiceFsm>) as OmitFirstParam<
typeof getResponse<TResponse>
>,
getState: fsm.getState,
subscribe: fsm.subscribe,
unsubscribe: fsm.unsubscribe,
fsm,
} as const;
};

// Demo order storage consumer
const orderStorageContextConsumer = serverContextConsumer<AlwatrDocumentStorage<Order>>('order_storage_context', {
url: '/api/order-list',
});
orderStorageContextConsumer.request({queryParameters: {userId: 123}});
orderStorageContextConsumer.getState().target;
orderStorageContextConsumer.getResponse()?.data[12].discountType;
13 changes: 13 additions & 0 deletions core/context/src/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type {requestServiceFsmConstructor} from './core.js';
import type {StringifyableFetchOptions} from '@alwatr/fetch';
import type {FsmTypeHelper} from '@alwatr/fsm';
import type {AlwatrServiceResponseSuccessWithMeta, StringifyableRecord} from '@alwatr/type';

export interface RequestServiceFsmContext<
TResponse extends AlwatrServiceResponseSuccessWithMeta = AlwatrServiceResponseSuccessWithMeta
> extends StringifyableRecord {
fetchOptions?: Partial<StringifyableFetchOptions>;
response?: TResponse;
}

export type RequestServiceFsm = FsmTypeHelper<typeof requestServiceFsmConstructor>;

0 comments on commit c001c58

Please sign in to comment.