Skip to content
Draft
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
227 changes: 34 additions & 193 deletions packages/model/src/clients/client-factory.ts
Original file line number Diff line number Diff line change
@@ -1,89 +1,24 @@
import type { generateRequestInfo } from './request-transform';
import type { CommonResponse } from './interceptor';
import type { ClientOptions, Params, FetchPolicy, RequestConfig } from './types';
import type { FetchPolicy } from './types';
import type { HydrationStatus } from '../store';
import { ReplaySubject } from 'rxjs';
import { MODE } from '../const';

import { createCache, hash } from '../cache';

import { deepMerge } from '../utils';
import { createInterceptor } from './interceptor';
import { type CommonExtraParams } from '..';


const DEFAULT_OPTIONS: ClientOptions = {
method: 'GET',
headers: {
'content-type': 'application/json',
},
timeout: 60 * 1000,
credentials: 'include',
};

// TODO后续再优化下逻辑写法,比如对于method的定义,需要定义好client与options的边界,拆分通用merge和转换成requestInit的部分……
function mergeClientOptionsAndParams(options: ClientOptions, params: Params): RequestConfig {
const {
timeout,
headers,
method,
credentials,
baseUrl,
} = options;
const url = baseUrl
? new URL(params.url || '/', baseUrl).href
: params.url || '';

const commonConfig = {
timeout: params.timeout || timeout,
headers: deepMerge({}, headers, params.headers),
credentials,
url: url || '',
variables: params.variables,
};

if ('method' in params) {
return {
...commonConfig,
method: params.method || method,
fetchPolicy: params.fetchPolicy,
};
}

if ('query' in params) {
return {
...commonConfig,
query: params.query,
fetchPolicy: params.fetchPolicy,
};
}

if ('mutation' in params) {
return {
...commonConfig,
mutation: params.mutation,
}
}

return commonConfig;
}

class TimeoutError extends Error {
constructor(msg: string) {
super(msg)
this.message = msg;
}
export type CustomClient<Params, Response, Variables extends Record<string, any>, ExtraParams extends CommonExtraParams> = {
transformRequestParams(variables: Variables, extraParams: ExtraParams, mode: typeof MODE): Params
hashVariables?(variables: Variables, hash: (v: any) => string): string
request(params: Params, mode: typeof MODE): Promise<Response>
}

export function clientFactory(
type: 'GQL' | 'REST',
createRequestInfo: typeof generateRequestInfo,
options?: ClientOptions,
) {
const opts = options
? deepMerge({} as ClientOptions, DEFAULT_OPTIONS, options)
: deepMerge({} as ClientOptions, DEFAULT_OPTIONS);
export function createCustomClient<Params, Response, Variables extends Record<string, any>, ExtraParams extends CommonExtraParams>(type: string, client: CustomClient<Params, Response, Variables, ExtraParams>, options: {
cache?: ReturnType<typeof createCache>
}) {
const requestInterceptor = createInterceptor<Params>('request');
const responseInterceptor = createInterceptor<CommonResponse>('response');
const responseInterceptor = createInterceptor<Response>('response');

const interceptors = {
request: {
Expand All @@ -94,164 +29,70 @@ export function clientFactory(
},
};

function request<T>(params: Params): Promise<T> {
function request<T>(variables: Variables, extraParams: ExtraParams): Promise<T> {
// 处理前置拦截器

const list = [...requestInterceptor.list];

const config = mergeClientOptionsAndParams(opts, params);
const params = client.transformRequestParams(variables, extraParams, MODE)

// 后面再做benchmark看看一个tick会差出来多少性能
let promise = Promise.resolve(config);
let promise = Promise.resolve(params);

while (list.length) {
const item = list.shift();
promise = promise.then(item?.onResolve, item?.onReject);
}

let request: ReturnType<typeof createRequestInfo>;

return promise.then(params => {
// TODO这里的
request = createRequestInfo(type, params);

if (!opts.fetch) {
// 避免Node环境下的判断,所以没法简化写=。=,因为window.fetch会触发一次RHS导致报错
if (MODE === 'SPA') {
// 小程序因为没有window,所以需要这里绕一下
if (typeof window !== 'undefined' && window.fetch) {
opts.fetch = (resource, options) => window.fetch(resource, options);
} else {
throw new Error('There is no useful "fetch" function');
}
} else if (MODE === 'SSR') {
if (globalThis.fetch) {
opts.fetch = (resource, options) => globalThis.fetch(resource, options);
} else {
throw new Error('There is no useful "fetch" function');
}
}
}

const {
url,
requestInit,
} = request;
const fetchPromise = opts.fetch!(url, requestInit);

const timeoutPromise = new Promise<DOMException | TimeoutError>((resolve) => {
setTimeout(
() => {
if (MODE ==='SSR') {
resolve(new TimeoutError('The request has been timeout'))
} else {
resolve(new DOMException('The request has been timeout'))
}
},
config.timeout,
);
});
return Promise.race([timeoutPromise, fetchPromise]);
}).then((res) => {
// 浏览器断网情况下有可能会是null
if (res === null) {
res = MODE === 'SSR'
? new TimeoutError('The request has been timeout')
: new DOMException('The request has been timeout');
}

let promise = client.request!(params, MODE);
const list = [...responseInterceptor.list];

// 用duck type绕过类型判断
if (!('status' in res)) {
let promise: Promise<any> = Promise.reject({
res,
request,
});
while (list.length) {
const transform = list.shift();
promise = promise.then(transform?.onResolve, transform?.onReject);
}
return promise;
}

const receiveType = res.headers.get('Content-Type')
|| (request.requestInit.headers as Record<string, string>)?.['Content-Type']
|| (request.requestInit.headers as Record<string, string>)?.['content-type']
|| 'application/json';

const commonInfo: CommonResponse = {
status: res.status,
statusText: res.statusText,
headers: res.headers,
config: request,
data: undefined,
};

let promise;
if (receiveType.indexOf('application/json') !== -1) {
promise = res.ok
? res.json().then(data => {
commonInfo.data = data;
return commonInfo;
})
: Promise.reject({
res,
request,
})
} else {
commonInfo.data = res.body;
// 其它类型就把body先扔回去……也许以后有用……
promise = res.ok ? Promise.resolve(commonInfo) : Promise.reject({
res,
request,
});
}
while (list.length) {
const transform = list.shift();
promise = promise.then(transform?.onResolve, transform?.onReject);
}

return promise;
});
return promise as any;
})
}

const cache = options?.cache || createCache();

function getDataFromCache<T>(params: Parameters<typeof request>[0]) {
const data = cache.get<T>(`${hash(params.url)}-${hash(params.variables || {})}`);
function getDataFromCache<T>(variables: Parameters<typeof request>[0]) {
const key = (client.hashVariables ?? hash)(variables, hash)
const data = cache.get<T>(key);
return data;
}

function setDataToCache<T>(params: Parameters<typeof request>[0], data: T) {
const key = `${hash(params.url)}-${hash(params.variables || {})}`;
function setDataToCache<T>(variables: Parameters<typeof request>[0], data: T) {
const key = (client.hashVariables ?? hash)(variables, hash)
cache.put(key, data);
}

function requestWithCache<T>(
params: Parameters<typeof request>[0],
variables: Parameters<typeof request>[0],
extraParams: Parameters<typeof request>[1],
fetchPolicy: FetchPolicy = 'network-first',
hydrationStatus: HydrationStatus,
): ReplaySubject<T> {
const subject = new ReplaySubject<T>();
// 处于Hydration阶段,一律先从缓存里面拿
if (hydrationStatus.value !== 2) {
const data = getDataFromCache<T>(params);
const data = getDataFromCache<T>(variables);
if (data) {
subject.next(data);
subject.complete();
return subject;
}
}
const data = getDataFromCache<T>(params);
const data = getDataFromCache<T>(variables);
switch (fetchPolicy) {
case 'cache-and-network':
if (data) {
subject.next(data);
}
request<T>(params).then(data => {
request<T>(variables, extraParams).then(data => {
// TODO还差分发network status出去
setDataToCache(params, data);
setDataToCache(variables, data);
subject.next(data);
subject.complete();
}).catch(e => {
Expand All @@ -263,17 +104,17 @@ export function clientFactory(
if (data) {
subject.next(data);
} else {
request<T>(params).then(data => {
request<T>(variables, extraParams).then(data => {
// TODO还差分发network status出去
setDataToCache(params, data);
setDataToCache(variables, data);
subject.next(data);
subject.complete();
}).catch(e => subject.error(e));
}
break;
case 'network-first':
request<T>(params).then(data => {
setDataToCache(params, data);
request<T>(variables, extraParams).then(data => {
setDataToCache(variables, data);
subject.next(data);
subject.complete();
}).catch(e => {
Expand All @@ -291,7 +132,7 @@ export function clientFactory(
}
break;
case 'network-only':
request<T>(params)
request<T>(variables, extraParams)
.then(data => {
subject.next(data);
subject.complete();
Expand All @@ -306,11 +147,11 @@ export function clientFactory(

return subject;
}

return {
interceptors,
query: requestWithCache,
mutate: request,
type
};
}

}
6 changes: 2 additions & 4 deletions packages/model/src/clients/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import type { ClientOptions } from './types';

import { clientFactory } from './client-factory';
import { generateRequestInfo } from './request-transform';
import { createRestClient } from './rest-client';

export type * from './types';

export function createClient(type: 'GQL' | 'REST', options?: ClientOptions) {
return clientFactory(type, generateRequestInfo, options)
return createRestClient(options)
}
Loading