diff --git a/packages/model/src/clients/client-factory.ts b/packages/model/src/clients/client-factory.ts index 79fe5fa..638dd11 100644 --- a/packages/model/src/clients/client-factory.ts +++ b/packages/model/src/clients/client-factory.ts @@ -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, 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 } -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, ExtraParams extends CommonExtraParams>(type: string, client: CustomClient, options: { + cache?: ReturnType +}) { const requestInterceptor = createInterceptor('request'); - const responseInterceptor = createInterceptor('response'); + const responseInterceptor = createInterceptor('response'); const interceptors = { request: { @@ -94,164 +29,70 @@ export function clientFactory( }, }; - function request(params: Params): Promise { + function request(variables: Variables, extraParams: ExtraParams): Promise { // 处理前置拦截器 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; - 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((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 = 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)?.['Content-Type'] - || (request.requestInit.headers as Record)?.['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(params: Parameters[0]) { - const data = cache.get(`${hash(params.url)}-${hash(params.variables || {})}`); + function getDataFromCache(variables: Parameters[0]) { + const key = (client.hashVariables ?? hash)(variables, hash) + const data = cache.get(key); return data; } - function setDataToCache(params: Parameters[0], data: T) { - const key = `${hash(params.url)}-${hash(params.variables || {})}`; + function setDataToCache(variables: Parameters[0], data: T) { + const key = (client.hashVariables ?? hash)(variables, hash) cache.put(key, data); } function requestWithCache( - params: Parameters[0], + variables: Parameters[0], + extraParams: Parameters[1], fetchPolicy: FetchPolicy = 'network-first', hydrationStatus: HydrationStatus, ): ReplaySubject { const subject = new ReplaySubject(); // 处于Hydration阶段,一律先从缓存里面拿 if (hydrationStatus.value !== 2) { - const data = getDataFromCache(params); + const data = getDataFromCache(variables); if (data) { subject.next(data); subject.complete(); return subject; } } - const data = getDataFromCache(params); + const data = getDataFromCache(variables); switch (fetchPolicy) { case 'cache-and-network': if (data) { subject.next(data); } - request(params).then(data => { + request(variables, extraParams).then(data => { // TODO还差分发network status出去 - setDataToCache(params, data); + setDataToCache(variables, data); subject.next(data); subject.complete(); }).catch(e => { @@ -263,17 +104,17 @@ export function clientFactory( if (data) { subject.next(data); } else { - request(params).then(data => { + request(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(params).then(data => { - setDataToCache(params, data); + request(variables, extraParams).then(data => { + setDataToCache(variables, data); subject.next(data); subject.complete(); }).catch(e => { @@ -291,7 +132,7 @@ export function clientFactory( } break; case 'network-only': - request(params) + request(variables, extraParams) .then(data => { subject.next(data); subject.complete(); @@ -306,11 +147,11 @@ export function clientFactory( return subject; } - return { interceptors, query: requestWithCache, mutate: request, type - }; + } + } diff --git a/packages/model/src/clients/index.ts b/packages/model/src/clients/index.ts index 4430ac9..b8cfed0 100644 --- a/packages/model/src/clients/index.ts +++ b/packages/model/src/clients/index.ts @@ -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) } diff --git a/packages/model/src/clients/rest-client.ts b/packages/model/src/clients/rest-client.ts new file mode 100644 index 0000000..9b1ad04 --- /dev/null +++ b/packages/model/src/clients/rest-client.ts @@ -0,0 +1,201 @@ +import { generateRequestInfo } from './request-transform'; +import type { CommonResponse } from './interceptor'; +import type { ClientOptions, Params, FetchPolicy, RequestConfig, RestParams, Method, HTTPHeaders } from './types'; +import { MODE } from '../const'; + +import { deepMerge } from '../utils'; +import { type CustomClient, createCustomClient } from './client-factory'; +import { type CommonExtraParams, createCache } 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: Partial): RestParams { + 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, + }; + } + + return commonConfig; +} + +export type RestVariables = { + url: string, + variables?: Record +} + +export type RestExtraOptions = CommonExtraParams & { + method?: Method +} + +export type RestResponse = CommonResponse +export type RestClient = CustomClient + +export type RestClientOptions = { + baseUrl?: string; + method?: Method; + credentials?: RequestCredentials; + headers?: HTTPHeaders; + timeout?: number; + fetch?: typeof fetch; + cache?: ReturnType; +}; + +class TimeoutError extends Error { + constructor(msg: string) { + super(msg) + this.message = msg; + } +} + +export function createRestClient(options?: RestClientOptions) { + const opts = options + ? deepMerge({} as ClientOptions, DEFAULT_OPTIONS, options) + : deepMerge({} as ClientOptions, DEFAULT_OPTIONS); + + const request: RestClient['request'] = (params, mode) => { + const request = generateRequestInfo('REST', 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((resolve) => { + setTimeout( + () => { + if (MODE === 'SSR') { + resolve(new TimeoutError('The request has been timeout')) + } else { + resolve(new DOMException('The request has been timeout')) + } + }, + params.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'); + } + + // 用duck type绕过类型判断 + if (!('status' in res)) { + return Promise.reject({ + res, + request, + }); + } + + const receiveType = res.headers.get('Content-Type') + || (request.requestInit.headers as Record)?.['Content-Type'] + || (request.requestInit.headers as Record)?.['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, + }); + } + + return promise; + }); + + } + + const transformRequestParams: RestClient['transformRequestParams'] = (variables, extraOptions, mode) => { + const params: Partial = { + ...extraOptions, + url: variables.url, + variables: variables.variables + } + return mergeClientOptionsAndParams(opts, params); + } + + const hash: RestClient['hashVariables'] = (variables, hash) => { + return `${hash(variables.url)}-${hash(variables.variables || {})}` + + } + + const client: CustomClient = { + transformRequestParams, + hashVariables: hash, + request + } + + return createCustomClient('REST', client, { + cache: opts.cache!, + }) +} diff --git a/packages/model/src/clients/types.ts b/packages/model/src/clients/types.ts index a81e6ee..656b839 100644 --- a/packages/model/src/clients/types.ts +++ b/packages/model/src/clients/types.ts @@ -13,6 +13,7 @@ export type Client = ReturnType; export type RebornClient = { gql?: Client, rest?: Client, + [key: string]: Client | undefined }; type MethodType = Lowercase | Uppercase; @@ -72,7 +73,7 @@ export type GQLMutationParams = { export type RestParams = { url: string; method?: Method; - + credentials?: RequestCredentials; headers?: HTTPHeaders; variables?: Record; timeout?: number; @@ -92,4 +93,4 @@ export type RestRequestConfig = RestParams & Omit | ReturnType> = []; +const tempQueryList: Array> = []; -export const useRestQuery = (options: RestQueryOptions) => { - const vm = getCurrentInstance(); +export const useQuery = (clientKey: string, options: CreateQueryOptions) => { + const vm = getCurrentInstance() if (creatingModelCount <= 0 || !vm) { - throw new Error(`You should use useRestQuery with createModel context `); + throw new Error(`You should use use${clientKey.toUpperCase()}Query with createModel context `); } + const route = vm.proxy!.$route; const { rebornClient: client, store } = getRootStore(); - const query = createRestQuery( + const query = createQuery( options, null, route, @@ -47,66 +52,96 @@ export const useRestQuery = (options: RestQueryOptions) => { status, loading, error, - data: data as Ref, + data: data as Ref, refetch: query.refetch, fetchMore: query.fetchMore, onNext: query.onNext, requestReason: query.requestReason }; -}; +} -export const useGQLQuery = (options: GQLQueryOptions) => { +export const useMutation = (clientKey: string, options: CreateMutationOptions) => { const vm = getCurrentInstance(); if (creatingModelCount <= 0 || !vm) { - throw new Error(`You should use useGQLQuery with createModel context `); + throw new Error(`You should use use${clientKey.toUpperCase()}Mutation with createModel context `); } - const route = vm.proxy!.$route; - const { rebornClient: client, store } = getRootStore(); + const { rebornClient: client } = getRootStore(); + return createMutation(options, null, route, client.rest); +} - const query = createGQLQuery(options, null, route, store.hydrationStatus, client.rest); - tempQueryList.push(query); - const status = useStatus(query.info, query.requestReason); - const { loading, error, data } = toRefs(query.info); +export const useRestQuery = (options: RestQueryOptions) => { + const createOptions: CreateQueryOptions = { + variables(route) { + const variables = typeof options.variables === 'function' ? (options.variables as VariablesFn).call(this, route) : options.variables + const url = typeof options.url === 'function' ?options.url.call(this, route, variables) : options.url + return { + variables, + url + } + }, + extraParams: { + headers: options.headers, + method: options.method, + credentials: options.credentials, + timeout: options.timeout + }, + prefetch: options.prefetch, + fetchPolicy: options.fetchPolicy, + skip: options.skip, + pollInterval: options.pollInterval, + updateQuery: options.updateQuery, + } + const query = useQuery('REST', createOptions) return { - info: query.info, - status, - loading, - error, - data: data as Ref, - refetch: query.refetch, - fetchMore: query.fetchMore, - onNext: query.onNext, - requestReason: query.requestReason + ...query, + fetchMore(variables: any) { + query.fetchMore({ + variables, + }) + } } +}; + +export const useGQLQuery = (options: QueryOptions) => { + return useQuery('GQL', options) } + export const useRestMutation = (options: RestMutationOptions) => { - const vm = getCurrentInstance(); - if (creatingModelCount <= 0 || !vm) { - throw new Error(`You should use useRestMutation with createModel context `); + const mutationOptions: CreateMutationOptions = { + variables(route, params) { + const variables = typeof options.variables === 'function' ? (options.variables as MutationVariablesFn).call(this, params, route) : options.variables + const url = typeof options.url === 'function' ?options.url.call(this, route, variables) : options.url + return { + variables, + url + } + }, + extraParams: { + headers: options.headers, + method: options.method, + credentials: options.credentials, + timeout: options.timeout + }, } - const route = vm.proxy!.$route; - const { rebornClient: client } = getRootStore(); + const mutation = useMutation('REST', mutationOptions) - return createRestMutation(options, null, route, client.rest); -} -export const useGQLMutation = (options: GQLMutationOptions) => { - const vm = getCurrentInstance(); - if (creatingModelCount <= 0 || !vm) { - throw new Error(`You should use useGQLMutation with createModel context `); + return { + ...mutation, } - const route = vm.proxy!.$route; - const { rebornClient: client } = getRootStore(); - return createGQLMutation(options, null, route, client.rest); } +export const useGQLMutation = (options: GQLMutationOptions) => { + return useMutation('GQL', options as any) +} + export type FNModelCreator = { type: string, - creator: () => { model: T, queryList: Array | ReturnType>}; + creator: () => { model: T, queryList: Array> }; } export function createModelFromCA( diff --git a/packages/model/src/operations/core.ts b/packages/model/src/operations/core.ts index 29e58d5..0b5794d 100644 --- a/packages/model/src/operations/core.ts +++ b/packages/model/src/operations/core.ts @@ -5,6 +5,7 @@ import type { GQLQueryOptions, RestMutationOptions, GQLMutationOptions, + CommonQueryOptions, } from './types'; import { RequestReason, type InfoDataType } from './status'; import type { RouteLocationNormalizedLoaded } from 'vue-router'; @@ -29,7 +30,7 @@ export function initDataType() { } export function generateQueryOptions( - option: RestQueryOptions | GQLQueryOptions, + option: CommonQueryOptions, route: RouteLocationNormalizedLoaded, model: ModelType, ) { diff --git a/packages/model/src/operations/gqlMutation.ts b/packages/model/src/operations/gqlMutation.ts index c93cb66..8e5f650 100644 --- a/packages/model/src/operations/gqlMutation.ts +++ b/packages/model/src/operations/gqlMutation.ts @@ -11,52 +11,5 @@ export function createGQLMutation( route: RouteLocationNormalizedLoaded, client?: Client, ) { - if (!client) { - throw new Error('No GQL Client has been set'); - } - const info = initDataType(); - function variables(params: T) { - if (option.variables && typeof option.variables === 'function') { - return (option.variables as MutationVariablesFn).call( - model, - params, - route, - ); - } - return params; - } - - function url>(params: T) { - if (option.url && typeof option.url === 'function') { - return option.url.call( - model, - route, - params, - ); - } - return option.url; - } - - return { - info, - mutate(params: any) { - info.loading = true; - info.error = undefined; - - return client.mutate({ - // TODO - mutation: '' as unknown as any, - variables: variables(params), - }).then(data => { - info.error = undefined; - if (data) { - info.data = data; - } - info.loading = false; - }).catch(e => { - info.error = e; - info.loading = false; - }); - } - }; + return {} as any } diff --git a/packages/model/src/operations/mutation.ts b/packages/model/src/operations/mutation.ts new file mode 100644 index 0000000..e2f8854 --- /dev/null +++ b/packages/model/src/operations/mutation.ts @@ -0,0 +1,51 @@ +import type { CreateMutationOptions, MutationVariable } from './types'; +import type { Client } from '../clients'; +import type { RouteLocationNormalizedLoaded } from 'vue-router'; + +import { initDataType } from './core'; + +export function createMutation( + option: CreateMutationOptions, + model: ModelType, + route: RouteLocationNormalizedLoaded, + client?: Client, +){ + + if (!client) { + throw new Error('No Rest Client has been set'); + } + const info = initDataType(); + + function variables(params: T) { + if (option.variables && typeof option.variables === 'function') { + return (option.variables as MutationVariable).call( + model, + params, + route, + ); + } + return params; + } + + + function mutate>(params: T) { + info.loading = true; + info.error = undefined; + + return client!.mutate(variables(params), option.extraParams).then(data => { + info.error = undefined; + if (data) { + info.data = data; + } + info.loading = false; + }).catch(e => { + info.error = e; + info.loading = false; + }); + } + + return { + info, + mutate, + }; +} diff --git a/packages/model/src/operations/query.ts b/packages/model/src/operations/query.ts new file mode 100644 index 0000000..dee9949 --- /dev/null +++ b/packages/model/src/operations/query.ts @@ -0,0 +1,192 @@ +import type { RouteLocationNormalizedLoaded } from 'vue-router'; +import { map, merge, Observable, type Subscription } from 'rxjs'; + +import type { CreateQueryOptions, QueryOptions, QueryVariable } from './types'; +import type { Client, } from '../clients'; +import type { HydrationStatus } from '../store'; + +import { computed, ref, watch, type Ref } from 'vue'; +import { deepMerge, fromWatch } from '../utils'; +import { RequestReason } from './status'; +import { initDataType } from './core'; + +function generateQueryOptions( + option: CreateQueryOptions, + route: RouteLocationNormalizedLoaded, + model: ModelType, +) { + const info = initDataType(); + const skip = computed(() => { + if (typeof option.skip === 'function') { + return option.skip.call(model, route); + } + return !!option.skip; + }); + + const pollInterval = computed(() => { + if (typeof option.pollInterval === 'function') { + return option.pollInterval.call(model, route); + } + return option.pollInterval || 0; + }); + + const variables = computed(() => { + if (typeof option.variables === 'function') { + return (option.variables as QueryVariable).call(model, route); + } + return option.variables; + }); + + + const variables$ = fromWatch(() => variables.value, { immediate: true }).pipe(map(i => RequestReason.setVariables)) + const pollInterval$ = new Observable(subscriber => { + let timeout: ReturnType; + watch(() => pollInterval.value, (val, oldVal) => { + if (val === oldVal) { + return; + } + if (val <= 0) { + clearTimeout(timeout); + return; + } + + poll(); + }, { immediate: true }) + const doInterval = () => { + subscriber.next(RequestReason.poll); + poll(); + } + + const poll = () => { + clearTimeout(timeout); + setTimeout(doInterval, pollInterval.value); + } + }); + + const fetchQuery$ = merge( + variables$, + pollInterval$, + ); + + return { + info, + skip, + pollInterval, + variables, + fetchQuery$, + prefetch: option.prefetch || true, + }; +} + +export function createQuery( + option: CreateQueryOptions, + model: ModelType, + route: RouteLocationNormalizedLoaded, + hydrationStatus: HydrationStatus, + client?: Client, +) { + if (!client) { + throw new Error('No Rest Client has been set'); + } + const { info, skip, variables, fetchQuery$, prefetch } = generateQueryOptions( + option, + route, + model, + ); + + const genParams = (additionVariables?: any) => { + return { + variables: { ...variables.value, ...additionVariables ?? {} }, + extraParams: option.extraParams, + } + } + + function fetch() { + info.loading = true; + return new Promise((resolve) => { + const clientParams = genParams() + // TODO后面再重写一下 + const subject = client!.query(clientParams.variables, clientParams.extraParams, option.fetchPolicy, hydrationStatus); + subject.subscribe({ + next: (data) => { + info.error = undefined; + if (data) { + info.data = data; + } + info.loading = false; + resolve(undefined); + }, + error: (e) => { + info.error = e; + info.loading = false; + resolve(undefined); + }, + complete: () => { + // TODO先临时搞一下,后面再看怎么串一下Observable + subject.unsubscribe(); + }, + }); + }); + } + + let sub: Subscription | null = null; + + const requestReason = ref(RequestReason.setVariables); + function init() { + sub = fetchQuery$.subscribe((reason) => { + if (skip.value) { + return; + } + requestReason.value = reason; + fetch(); + }); + } + + function destroy() { + if (sub) { + sub.unsubscribe(); + sub = null; + } + } + + function fetchMore(variables: ReturnType) { + return new Promise((resolve) => { + requestReason.value = RequestReason.fetchMore + info.loading = true; + const params = genParams(variables) + + const observable = client!.query(params.variables, params.extraParams, option.fetchPolicy, hydrationStatus); + observable.subscribe({ + next: (data) => { + info.error = undefined; + info.data = data && option.updateQuery ? option.updateQuery(info.data, data) : data; + info.loading = false; + resolve(undefined); + }, + error: (e) => { + info.error = e; + info.loading = false; + resolve(undefined); + }, + }); + }); + } + + function onNext(sub: (params: { data: DataType; loading: boolean; error: any }) => void) { + // TODO + } + + return { + info, + init, + refetch: () => { + requestReason.value = RequestReason.refetch; + fetch(); + }, + prefetch: fetch, + fetchMore, + destroy, + onNext, + requestReason, + }; +} diff --git a/packages/model/src/operations/restMutation.ts b/packages/model/src/operations/restMutation.ts index 151f17c..024cf4c 100644 --- a/packages/model/src/operations/restMutation.ts +++ b/packages/model/src/operations/restMutation.ts @@ -10,56 +10,5 @@ export function createRestMutation( route: RouteLocationNormalizedLoaded, client?: Client, ){ - - if (!client) { - throw new Error('No Rest Client has been set'); - } - const info = initDataType(); - function variables(params: T) { - if (option.variables && typeof option.variables === 'function') { - return (option.variables as MutationVariablesFn).call( - model, - params, - route, - ); - } - return params; - } - - function url>(params: T) { - if (option.url && typeof option.url === 'function') { - return option.url.call( - model, - route, - params, - ); - } - return option.url; - } - - function mutate>(params: T) { - info.loading = true; - info.error = undefined; - return client!.mutate({ - url: url(variables(params)), - headers: option.headers, - method: option.method, - variables: variables(params), - timeout: option.timeout, - }).then(data => { - info.error = undefined; - if (data) { - info.data = data; - } - info.loading = false; - }).catch(e => { - info.error = e; - info.loading = false; - }); - } - - return { - info, - mutate, - }; +return {} as any } diff --git a/packages/model/src/operations/restQuery.ts b/packages/model/src/operations/restQuery.ts index ded22f5..ef3a4ef 100644 --- a/packages/model/src/operations/restQuery.ts +++ b/packages/model/src/operations/restQuery.ts @@ -1,7 +1,7 @@ import type { RouteLocationNormalizedLoaded } from 'vue-router'; import { Observable, type Subscription } from 'rxjs'; -import type { RestQueryOptions, RestFetchMoreOption } from './types'; +import type { RestQueryOptions, RestFetchMoreOption, CommonQueryOptions } from './types'; import type { Client, RestRequestConfig } from '../clients'; import type { HydrationStatus, Store } from '../store'; @@ -10,131 +10,6 @@ import { computed, ref, type Ref } from 'vue'; import { deepMerge } from '../utils'; import { RequestReason } from './status'; -export function createRestQuery( - option: RestQueryOptions, - model: ModelType, - route: RouteLocationNormalizedLoaded, - hydrationStatus: HydrationStatus, - client?: Client, -) { - if (!client) { - throw new Error('No Rest Client has been set'); - } - const { info, skip, variables, fetchQuery$, prefetch } = generateQueryOptions( - option, - route, - model, - ); - - const url = computed(() => { - if (typeof option.url === 'function') { - return option.url.call(model, route, variables.value); - } - return option.url; - }); - - function fetch() { - info.loading = true; - return new Promise((resolve) => { - const clientParams = { - url: url.value, - headers: option.headers, - method: option.method, - fetchPolicy: option.fetchPolicy, - variables: variables.value, - timeout: option.timeout, - }; - // TODO后面再重写一下 - const subject = client!.query(clientParams, option.fetchPolicy, hydrationStatus); - subject.subscribe({ - next: (data) => { - info.error = undefined; - if (data) { - info.data = data; - } - info.loading = false; - resolve(undefined); - }, - error: (e) => { - info.error = e; - info.loading = false; - resolve(undefined); - }, - complete: () => { - // TODO先临时搞一下,后面再看怎么串一下Observable - subject.unsubscribe(); - }, - }); - }); - } - - let sub: Subscription | null = null; - - const requestReason = ref(RequestReason.setVariables); - function init() { - sub = fetchQuery$.subscribe((reason) => { - if (skip.value) { - return; - } - requestReason.value = reason; - fetch(); - }); - } - - function destroy() { - if (sub) { - sub.unsubscribe(); - sub = null; - } - } - - function fetchMore(variables: RestFetchMoreOption['variables']) { - return new Promise((resolve) => { - requestReason.value = RequestReason.fetchMore - info.loading = true; - const params: RestRequestConfig = { - url: url.value, - method: option.method, - variables, - timeout: option.timeout, - }; - - if (option.headers) { - params.headers = deepMerge({}, params.headers || {}, option.headers); - } - - const observable = client!.query(params, option.fetchPolicy, hydrationStatus); - observable.subscribe({ - next: (data) => { - info.error = undefined; - info.data = data && option.updateQuery ? option.updateQuery(info.data, data) : data; - info.loading = false; - resolve(undefined); - }, - error: (e) => { - info.error = e; - info.loading = false; - resolve(undefined); - }, - }); - }); - } - - function onNext(sub: (params: { data: DataType; loading: boolean; error: any }) => void) { - // TODO - } - - return { - info, - init, - refetch: () => { - requestReason.value = RequestReason.refetch; - fetch(); - }, - prefetch: fetch, - fetchMore, - destroy, - onNext, - requestReason, - }; +export function createRestQuery(...args: any[]) { + return {} as any } diff --git a/packages/model/src/operations/types.ts b/packages/model/src/operations/types.ts index 5c8da75..34fff71 100644 --- a/packages/model/src/operations/types.ts +++ b/packages/model/src/operations/types.ts @@ -9,7 +9,7 @@ export type NumberFn = (this: T, route: RouteLocationNormalizedLoaded) => num export type UrlFn = (this: T, route: RouteLocationNormalizedLoaded, variables: Record | undefined) => string; // 和CreateQuery有关的参数部分 -type CommonQueryOptions = { +export type CommonQueryOptions = { prefetch?: boolean; fetchPolicy?: FetchPolicy; credentials?: RequestCredentials; @@ -67,3 +67,47 @@ export type MutationResult = { mutate(args0: T, args1?: any): Promise; error: any; } + + +export type CommonExtraParams = { + credentials?: RequestCredentials; + headers?: HTTPHeaders; + timeout?: number +} + + + + +export type MutationVariable = ((this: T, params: any, route: RouteLocationNormalizedLoaded) => R) +export type MutationOptions> = { + extraParams: P, + variables: Record> +} + +export type CreateMutationOptions = Record> = { + extraParams: P, + variables: MutationVariable +} + + + +export type QueryVariable = ((this: T,route: RouteLocationNormalizedLoaded) => R) +export type QueryOptions = Record> = { + extraParams: P, + variables: V + prefetch?: boolean; + fetchPolicy?: FetchPolicy; + skip?: BooleanFn | boolean; + pollInterval?: NumberFn | number; + updateQuery?(prev?: DataType, next?: DataType): DataType; +} + +export type CreateQueryOptions = Record> = { + extraParams: P, + variables: QueryVariable, + prefetch?: boolean; + fetchPolicy?: FetchPolicy; + skip?: BooleanFn | boolean; + pollInterval?: NumberFn | number; + updateQuery?(prev?: DataType, next?: DataType): DataType; +} diff --git a/packages/model/src/store/types.ts b/packages/model/src/store/types.ts index b66e32c..7ebcb6a 100644 --- a/packages/model/src/store/types.ts +++ b/packages/model/src/store/types.ts @@ -1,14 +1,14 @@ -import type { createGQLQuery, createRestQuery } from '../operations'; import type { FNModelCreator, Constructor, OriginalModelInstance } from '../model'; import type { EffectScope } from 'vue'; +import { createQuery } from '../operations/query'; export type ModelInfo = { constructor: FNModelCreator | Constructor; instance: OriginalModelInstance | null; count: number; - queryList: Array>; + queryList: Array>; scope: EffectScope | null; } -export type ModelMap = Record \ No newline at end of file +export type ModelMap = Record