From 464b31bd6713a21957c297fcc2fa03ea180a2b7d Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sun, 28 Jun 2020 11:52:45 +0100 Subject: [PATCH] feat: allow direct access to `fetch` --- .eslintrc.js | 2 + src/cache.ts | 135 +++++++++++++++++++++++++++------------------ src/index.ts | 17 +++++- src/query.ts | 13 ++++- test/cache.spec.ts | 7 +-- test/image.spec.ts | 7 +-- test/index.spec.ts | 2 +- test/query.spec.ts | 27 +++++++-- test/ssr.spec.ts | 5 +- test/utils.spec.ts | 34 ++++++++++++ 10 files changed, 172 insertions(+), 77 deletions(-) create mode 100644 test/utils.spec.ts diff --git a/.eslintrc.js b/.eslintrc.js index 25f912d7..aca9c016 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,6 +6,8 @@ module.exports = { rules: { '@typescript-eslint/no-inferrable-types': 1, '@typescript-eslint/explicit-function-return-type': 0, + '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/explicit-module-boundary-types': 0, }, extends: ['@siroc'], } diff --git a/src/cache.ts b/src/cache.ts index 1ef8addf..5c8b945f 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -11,7 +11,7 @@ import { /** * Cached data, status of fetch, timestamp of last fetch, error */ -type CacheEntry = [T, FetchStatus, number, any] +type CacheEntry = [T, FetchStatus, number, any, Promise] const cache = reactive>>({}) @@ -21,6 +21,13 @@ export function ensureInstance() { return instance } +export function getServerInstance() { + const instance = getCurrentInstance() + + if (instance?.$isServer) return instance + return false +} + export type FetchStatus = | 'initialised' | 'loading' @@ -28,6 +35,15 @@ export type FetchStatus = | 'client loaded' | 'error' +interface SetCacheOptions { + key: string + value?: T | K + status?: FetchStatus + error?: any + promise?: Promise + time?: number +} + export interface CacheOptions { initialValue?: K /** @@ -55,9 +71,6 @@ export function useCache( fetcher: (key: string) => Promise, options: CacheOptions = {} ) { - const instance = ensureInstance() - const isServer = instance.$isServer - const { initialValue = null, deduplicate = false, @@ -67,89 +80,103 @@ export function useCache( const enableSSR = !clientOnly && strategy !== 'client' - function initialiseCache( - key: string, - value: any, - status: FetchStatus = 'initialised', + function initialiseCache({ + key, + value, + error = null, + status = 'initialised', time = new Date().getTime(), - error = null - ) { + }: SetCacheOptions) { Vue.set(cache, key, [value, status, time, error]) } - if (enableSSR && !isServer) { + const serverInstance = getServerInstance() + if (enableSSR && !serverInstance) { const prefetchState = (window as any).__VSANITY_STATE__ || ((window as any).__NUXT__ && (window as any).__NUXT__.vsanity) if (prefetchState && prefetchState[key.value]) { - initialiseCache( - key.value, - ...(prefetchState[key.value] as CacheEntry) - ) + const [value, status, time, error] = prefetchState[ + key.value + ] as CacheEntry + + initialiseCache({ + key: key.value, + value, + status, + time, + error, + }) } } function verifyKey(key: string) { const emptyCache = !(key in cache) - if (emptyCache) initialiseCache(key, initialValue) + if (emptyCache) initialiseCache({ key, value: initialValue as K }) return emptyCache || cache[key][1] === 'initialised' } - function setCache( - key: string, - value: any = (cache[key] && cache[key][0]) || initialValue, - status: FetchStatus = cache[key] && cache[key][1], - error: any = null - ) { - if (!(key in cache)) initialiseCache(key, value, status) + function setCache({ + key, + value = cache[key]?.[0] || initialValue, + status = cache[key]?.[1], + error = null, + promise = cache[key]?.[4], + }: SetCacheOptions) { + if (!(key in cache)) initialiseCache({ key, value, status }) + Vue.set(cache[key], 0, value) Vue.set(cache[key], 1, status) Vue.set(cache[key], 2, new Date().getTime()) Vue.set(cache[key], 3, error) + Vue.set(cache[key], 4, promise) } - async function fetch(query = key.value, force?: boolean) { + function fetch(query = key.value, force?: boolean) { if ( !force && deduplicate && - cache[query][1] === 'loading' && + cache[query]?.[1] === 'loading' && (deduplicate === true || - deduplicate < new Date().getTime() - cache[query][2]) + deduplicate < new Date().getTime() - cache[query]?.[2]) ) - return - - try { - setCache(query, undefined, 'loading') - - setCache( - query, - await fetcher(query), - isServer ? 'server loaded' : 'client loaded' + return Promise.resolve(cache[query][4] || initialValue) as Promise + + const promise = fetcher(query) + setCache({ key: query, status: 'loading', promise }) + + promise + .then(value => + setCache({ + key: query, + value, + status: serverInstance ? 'server loaded' : 'client loaded', + }) ) - } catch (e) { - setCache(query, undefined, 'error', e) - } + .catch(error => setCache({ key: query, status: 'error', error })) + return promise } - if (enableSSR && isServer) { - if (instance.$ssrContext) { - if (instance.$ssrContext.nuxt && !instance.$ssrContext.nuxt.vsanity) { - instance.$ssrContext.nuxt.vsanity = {} - } else if (!instance.$ssrContext.vsanity) { - instance.$ssrContext.vsanity = {} + if (enableSSR && serverInstance) { + const ctx = serverInstance.$ssrContext + if (ctx) { + if (ctx.nuxt && !ctx.nuxt.vsanity) { + ctx.nuxt.vsanity = {} + } else if (!ctx.vsanity) { + ctx.vsanity = {} } } onServerPrefetch(async () => { - await fetch(key.value, verifyKey(key.value)) - if ( - instance.$ssrContext && - !['loading', 'initialised'].includes(cache[key.value][1]) - ) { - if (instance.$ssrContext.nuxt) { - instance.$ssrContext.nuxt.vsanity[key.value] = cache[key.value] + try { + await fetch(key.value, verifyKey(key.value)) + // eslint-disable-next-line + } catch {} + if (ctx && !['loading', 'initialised'].includes(cache[key.value]?.[1])) { + if (ctx.nuxt) { + ctx.nuxt.vsanity[key.value] = cache[key.value] } else { - instance.$ssrContext.vsanity[key.value] = cache[key.value] + ctx.vsanity[key.value] = cache[key.value] } } }) @@ -175,10 +202,10 @@ export function useCache( watch( key, - async key => { + key => { if (strategy === 'server' && status.value === 'server loaded') return - await fetch(key, verifyKey(key)) + fetch(key, verifyKey(key)) }, { immediate: true } ) diff --git a/src/index.ts b/src/index.ts index 98e767eb..0b0f8f6d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,18 @@ -import { provide } from '@vue/composition-api' +import { provide, inject } from '@vue/composition-api' import { ClientConfig } from '@sanity/client' import { useCache, ensureInstance } from './cache' +import type { FetchStatus, CacheOptions } from './cache' import { useSanityImage, imageBuilderSymbol } from './image' import { useSanityFetcher, useSanityQuery, - Client, - Options, clientSymbol, previewClientSymbol, optionsSymbol, } from './query' +import type { Client, Options } from './query' interface RequiredConfig { /** @@ -64,4 +64,15 @@ export function useCustomClient(client: Client, defaultOptions: Options = {}) { provide(optionsSymbol, defaultOptions) } +export function fetch(query: string) { + ensureInstance() + const client = inject(clientSymbol) + if (!client) + throw new Error( + 'You must call useSanityClient before using sanity resources in this project.' + ) + return client.fetch(query) +} + export { useCache, useSanityFetcher, useSanityImage, useSanityQuery } +export type { Options, FetchStatus, CacheOptions } diff --git a/src/query.ts b/src/query.ts index 4f52da7d..6e7e57e9 100644 --- a/src/query.ts +++ b/src/query.ts @@ -35,6 +35,14 @@ interface Result { * The status of the query. Can be 'server loaded', 'loading', 'client loaded' or 'error'. */ status: Ref + /** + * An error returned in the course of fetching + */ + error: any + /** + * Get result directly from fetcher (integrates with cache) + */ + fetch: () => Promise } export type Options = Omit, 'initialValue'> & { @@ -101,7 +109,10 @@ export function useSanityFetcher( query => { const subscription = previewClient .listen(query, listenOptions) - .subscribe(event => event.result && setCache(query, event.result)) + .subscribe( + event => + event.result && setCache({ key: query, value: event.result }) + ) const unwatch = watch( computedQuery, diff --git a/test/cache.spec.ts b/test/cache.spec.ts index 4de6105c..38694cef 100644 --- a/test/cache.spec.ts +++ b/test/cache.spec.ts @@ -1,11 +1,8 @@ -import Vue from 'vue' -import CompositionApi, { ref, watch } from '@vue/composition-api' +import { ref, watch } from '@vue/composition-api' -import { useCache } from '..' +import { useCache } from '../src' import { runInSetup } from './helpers/mount' -Vue.use(CompositionApi) - jest.setTimeout(10000) describe('cache', () => { diff --git a/test/image.spec.ts b/test/image.spec.ts index 36b9ba31..5ac36b5e 100644 --- a/test/image.spec.ts +++ b/test/image.spec.ts @@ -1,10 +1,7 @@ -import Vue from 'vue' -import CompositionApi, { ref } from '@vue/composition-api' +import { ref } from '@vue/composition-api' import { runInSetup } from './helpers/mount' -import { useSanityImage, useSanityClient } from '..' - -Vue.use(CompositionApi) +import { useSanityImage, useSanityClient } from '../src' const config = { projectId: 'id', diff --git a/test/index.spec.ts b/test/index.spec.ts index ad22423c..38783945 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,7 +1,7 @@ import Vue from 'vue' import CompositionApi from '@vue/composition-api' -import { useSanityClient } from '..' +import { useSanityClient } from '../src' import { runInSetup } from './helpers/mount' Vue.config.productionTip = false diff --git a/test/query.spec.ts b/test/query.spec.ts index d2ef2770..4cd2c569 100644 --- a/test/query.spec.ts +++ b/test/query.spec.ts @@ -1,5 +1,4 @@ -import Vue from 'vue' -import CompositionApi, { ref } from '@vue/composition-api' +import { ref } from '@vue/composition-api' import flushPromises from 'flush-promises' import { defineDocument } from 'sanity-typed-queries' @@ -8,11 +7,10 @@ import { useSanityFetcher, useSanityClient, useSanityQuery, -} from '..' + fetch as _fetch, +} from '../src' import { runInSetup } from './helpers/mount' -Vue.use(CompositionApi) - const config = { projectId: 'id', dataset: 'production', @@ -87,6 +85,25 @@ describe('fetcher', () => { expect(results.value.status).toBe('server loaded') }) + test('allows direct access to client', async () => { + const result = await runInSetup(() => { + useCustomClient({ fetch: async t => `fetched-${t}` }) + const data = _fetch('test') + return { data } + }) + expect(await result.value.data).toBe('fetched-test') + const errored = await runInSetup(() => { + let data = false + try { + _fetch('test') + } catch { + data = true + } + return { data } + }) + expect(errored.value.data).toBe(true) + }) + test('allows custom client to be provided', async () => { const result = await runInSetup(() => { useCustomClient({ fetch: async t => `fetched-${t}` }) diff --git a/test/ssr.spec.ts b/test/ssr.spec.ts index ed5b8091..18f24fa3 100644 --- a/test/ssr.spec.ts +++ b/test/ssr.spec.ts @@ -3,13 +3,12 @@ */ import Vue from 'vue' -import VueCompositionApi, { ref, createElement } from '@vue/composition-api' +import { ref, createElement } from '@vue/composition-api' import { createRenderer } from 'vue-server-renderer' import { fetcher } from './helpers/utils' -import { useCache } from '..' +import { useCache } from '../src' -Vue.use(VueCompositionApi) Vue.config.productionTip = false Vue.config.devtools = false diff --git a/test/utils.spec.ts b/test/utils.spec.ts new file mode 100644 index 00000000..d6b44cf6 --- /dev/null +++ b/test/utils.spec.ts @@ -0,0 +1,34 @@ +import { ensureInstance, getServerInstance } from '../src/cache' +import { runInSetup } from './helpers/mount' + +describe('ensureInstance', () => { + it('returns component instance', async () => { + const data = await runInSetup(() => { + const vm = ensureInstance() + return { vm } + }) + + expect(data.value.vm).toBeDefined() + }) + it('errors when called out of setup', async () => { + let error = false + try { + ensureInstance() + } catch { + error = true + } + + expect(error).toBe(true) + }) +}) + +describe('getServerInstance', () => { + it('returns false when not on server', async () => { + const data = await runInSetup(() => { + const vm = getServerInstance() + return { vm } + }) + + expect(data.value.vm).toBe(false) + }) +})