Skip to content

Commit

Permalink
feat: allow direct access to fetch
Browse files Browse the repository at this point in the history
  • Loading branch information
danielroe committed Jun 28, 2020
1 parent 1a76462 commit 464b31b
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 77 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
}
135 changes: 81 additions & 54 deletions src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
/**
* Cached data, status of fetch, timestamp of last fetch, error
*/
type CacheEntry<T> = [T, FetchStatus, number, any]
type CacheEntry<T> = [T, FetchStatus, number, any, Promise<T>]

const cache = reactive<Record<string, CacheEntry<any>>>({})

Expand All @@ -21,13 +21,29 @@ export function ensureInstance() {
return instance
}

export function getServerInstance() {
const instance = getCurrentInstance()

if (instance?.$isServer) return instance
return false
}

export type FetchStatus =
| 'initialised'
| 'loading'
| 'server loaded'
| 'client loaded'
| 'error'

interface SetCacheOptions<T, K> {
key: string
value?: T | K
status?: FetchStatus
error?: any
promise?: Promise<T | K>
time?: number
}

export interface CacheOptions<K> {
initialValue?: K
/**
Expand Down Expand Up @@ -55,9 +71,6 @@ export function useCache<T, K = null>(
fetcher: (key: string) => Promise<T>,
options: CacheOptions<K> = {}
) {
const instance = ensureInstance()
const isServer = instance.$isServer

const {
initialValue = null,
deduplicate = false,
Expand All @@ -67,89 +80,103 @@ export function useCache<T, K = null>(

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<T, K>) {
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<any>)
)
const [value, status, time, error] = prefetchState[
key.value
] as CacheEntry<T>

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<T, K>) {
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<T>

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]
}
}
})
Expand All @@ -175,10 +202,10 @@ export function useCache<T, K = null>(

watch(
key,
async key => {
key => {
if (strategy === 'server' && status.value === 'server loaded') return

await fetch(key, verifyKey(key))
fetch(key, verifyKey(key))
},
{ immediate: true }
)
Expand Down
17 changes: 14 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand Down Expand Up @@ -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 }
13 changes: 12 additions & 1 deletion src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ interface Result<T> {
* The status of the query. Can be 'server loaded', 'loading', 'client loaded' or 'error'.
*/
status: Ref<FetchStatus>
/**
* An error returned in the course of fetching
*/
error: any
/**
* Get result directly from fetcher (integrates with cache)
*/
fetch: () => Promise<T>
}

export type Options = Omit<CacheOptions<any>, 'initialValue'> & {
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 2 additions & 5 deletions test/cache.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down
7 changes: 2 additions & 5 deletions test/image.spec.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
2 changes: 1 addition & 1 deletion test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
27 changes: 22 additions & 5 deletions test/query.spec.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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',
Expand Down Expand Up @@ -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}` })
Expand Down
5 changes: 2 additions & 3 deletions test/ssr.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading

0 comments on commit 464b31b

Please sign in to comment.