Repository: Kong/swrv (ts)

Issue:
Allow array as key, multiple arguments to fetcher
In the [parameter documentation](https://github.com/Kong/swrv#parameters), the readme mentions that it is possible to pass an array as a cache key to useSWRV, presumably to allow passing multiple arguments to the fetch function as SWR does.

Currently, this does not work and it brakes the caching completely. 

Is there a plan to support this feature, or is there another way to pass multiple arguments to the fetcher function?

Pull request description:
feat(args) add array/object as key
* adds serialize/hash for cache
* parity with swr api

Fixes #55

Patch:
diff --git a/src/lib/cache.ts b/src/lib/cache.ts
index 83abd8f..dd46a48 100644
--- a/src/lib/cache.ts
+++ b/src/lib/cache.ts
@@ -1,9 +1,32 @@
+import { IKey } from '@/types'
+import hash from './hash'
+
 interface ICacheItem {
   data: any,
   createdAt: number,
   expiresAt: number
 }
 
+function serializeKeyDefault (key: IKey): string {
+  if (typeof key === 'function') {
+    try {
+      key = key()
+    } catch (err) {
+      // dependencies not ready
+      key = ''
+    }
+  }
+
+  if (Array.isArray(key)) {
+    key = hash(key)
+  } else {
+    // convert null to ''
+    key = String(key || '')
+  }
+
+  return key
+}
+
 export default class SWRVCache {
   private ttl: number
   private items: Map<string, ICacheItem>
@@ -13,14 +36,20 @@ export default class SWRVCache {
     this.ttl = ttl
   }
 
+  serializeKey (key: IKey): string {
+    return serializeKeyDefault(key)
+  }
+
   /**
    * Get cache item while evicting
    */
   get (k: string): ICacheItem {
-    return this.items.get(k)
+    const _key = this.serializeKey(k)
+    return this.items.get(_key)
   }
 
   set (k: string, v: any, ttl: number) {
+    const _key = this.serializeKey(k)
     const timeToLive = ttl || this.ttl
     const now = Date.now()
     const item = {
@@ -32,13 +61,14 @@ export default class SWRVCache {
     timeToLive && setTimeout(() => {
       const current = Date.now()
       const hasExpired = current >= item.expiresAt
-      if (hasExpired) this.delete(k)
+      if (hasExpired) this.delete(_key)
     }, timeToLive)
 
-    this.items.set(k, item)
+    this.items.set(_key, item)
   }
 
   delete (k: string) {
-    this.items.delete(k)
+    const _key = this.serializeKey(k)
+    this.items.delete(_key)
   }
 }
diff --git a/src/lib/hash.ts b/src/lib/hash.ts
new file mode 100644
index 0000000..c4da7c6
--- /dev/null
+++ b/src/lib/hash.ts
@@ -0,0 +1,44 @@
+// From https://github.com/vercel/swr/blob/master/src/libs/hash.ts
+// use WeakMap to store the object->key mapping
+// so the objects can be garbage collected.
+// WeakMap uses a hashtable under the hood, so the lookup
+// complexity is almost O(1).
+const table = new WeakMap()
+
+// counter of the key
+let counter = 0
+
+// hashes an array of objects and returns a string
+export default function hash (args: any[]): string {
+  if (!args.length) return ''
+  let key = 'arg'
+  for (let i = 0; i < args.length; ++i) {
+    let _hash
+    if (
+      args[i] === null ||
+      (typeof args[i] !== 'object' && typeof args[i] !== 'function')
+    ) {
+      // need to consider the case that args[i] is a string:
+      // args[i]        _hash
+      // "undefined" -> '"undefined"'
+      // undefined   -> 'undefined'
+      // 123         -> '123'
+      // null        -> 'null'
+      // "null"      -> '"null"'
+      if (typeof args[i] === 'string') {
+        _hash = '"' + args[i] + '"'
+      } else {
+        _hash = String(args[i])
+      }
+    } else {
+      if (!table.has(args[i])) {
+        _hash = counter
+        table.set(args[i], counter++)
+      } else {
+        _hash = table.get(args[i])
+      }
+    }
+    key += '@' + _hash
+  }
+  return key
+}
diff --git a/src/types.ts b/src/types.ts
index 36c6908..703edfd 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -23,9 +23,10 @@ export interface IResponse<Data = any, Error = any> {
   mutate: () => Promise<void>
 }
 
-type keyFunction = () => string | null
+export type keyType = string | any[] | null
+type keyFunction = () => keyType
 
-export type IKey = keyFunction | string | null
+export type IKey = keyFunction | keyType
 
 export interface revalidateOptions {
   shouldRetryOnError?: boolean,
diff --git a/src/use-swrv.ts b/src/use-swrv.ts
index b540bf2..ca6d305 100644
--- a/src/use-swrv.ts
+++ b/src/use-swrv.ts
@@ -138,10 +138,20 @@ const mutate = async <Data>(key: string, res: Promise<Data> | Data, cache = DATA
 
 type StateRef<Data, Error> = { data: Data, error: Error, isValidating: boolean, revalidate: Function, key: any };
 
-/**
- * Stale-While-Revalidate hook to handle fetching, caching, validation, and more...
- */
-export default function useSWRV<Data = any, Error = any> (key: IKey, fn?: fetcherFn<Data>, config?: IConfig): IResponse<Data, Error> {
+/* Stale-While-Revalidate hook to handle fetching, caching, validation, and more... */
+function useSWRV<Data = any, Error = any>(
+  key: IKey
+): IResponse<Data, Error>
+function useSWRV<Data = any, Error = any>(
+  key: IKey,
+  fn?: fetcherFn<Data>,
+  config?: IConfig
+): IResponse<Data, Error>
+function useSWRV<Data = any, Error = any> (...args): IResponse<Data, Error> {
+  // console.log(args, args.length)
+  let key: IKey
+  let fn: fetcherFn<Data> | undefined
+  let config: IConfig = { ...defaultConfig }
   let unmounted = false
   let isHydrated = false
 
@@ -154,10 +164,20 @@ export default function useSWRV<Data = any, Error = any> (key: IKey, fn?: fetche
     vm.$vnode.elm.dataset &&
     vm.$vnode.elm.dataset.swrvKey)
 
-  config = {
-    ...defaultConfig,
-    ...config
+  if (args.length >= 1) {
+    key = args[0]
+  }
+  if (args.length >= 2) {
+    fn = args[1]
   }
+  if (args.length > 2) {
+    config = {
+      ...config,
+      ...args[2]
+    }
+  }
+
+  // console.log(key, fn, config)
 
   const ttl = IS_SERVER ? config.serverTTL : config.ttl
   const keyRef = typeof key === 'function' ? (key as any) : ref(key)
@@ -224,7 +244,8 @@ export default function useSWRV<Data = any, Error = any> (key: IKey, fn?: fetche
     const trigger = async () => {
       const promiseFromCache = PROMISES_CACHE.get(keyVal)
       if (!promiseFromCache) {
-        const newPromise = fetcher(keyVal)
+        const fetcherArgs = Array.isArray(keyVal) ? keyVal : [keyVal]
+        const newPromise = fetcher(...fetcherArgs)
         PROMISES_CACHE.set(keyVal, newPromise, config.dedupingInterval)
         await mutate(keyVal, newPromise, config.cache, ttl)
       } else {
@@ -374,3 +395,4 @@ function isPromise<T> (p: any): p is Promise<T> {
 }
 
 export { mutate }
+export default useSWRV


Test patch:
diff --git a/tests/use-swrv.spec.tsx b/tests/use-swrv.spec.tsx
index b85658c..e2ac13c 100644
--- a/tests/use-swrv.spec.tsx
+++ b/tests/use-swrv.spec.tsx
@@ -14,7 +14,7 @@ const tick: Function = async (vm, times) => {
 }
 
 describe('useSWRV', () => {
-  it('should return data on hydration when fetch is not a promise', done => {
+  it('should return data on hydration when fetch is not a promise', async done => {
     const fetch = () => 'SWR'
     const vm = new Vue({
       template: `<div>hello, {{ data }}</div>`,
@@ -83,6 +83,32 @@ describe('useSWRV', () => {
     done()
   })
 
+  it('should accept object args', async () => {
+    const obj = { v: 'hello' }
+    const arr = ['world']
+
+    const vm = new Vue({
+      template: `<div>{{v1}}, {{v2}}, {{v3}}</div>`,
+      setup  () {
+        const { data: v1 } = useSWRV(['args-1', obj, arr], (a, b, c) => {
+          return a + b.v + c[0]
+        })
+
+        // reuse the cache
+        const { data: v2 } = useSWRV(['args-1', obj, arr], () => 'not called!')
+
+        // different object
+        const { data: v3 } = useSWRV(['args-2', obj, 'world'], (a, b, c) => {
+          return a + b.v + c
+        })
+
+        return { v1, v2, v3 }
+      }
+    }).$mount()
+
+    expect(vm.$el.textContent).toBe(`args-1helloworld, args-1helloworld, args-2helloworld`)
+  })
+
   it('should allow async fetcher functions', async done => {
     const vm = new Vue({
       template: `<div>hello, {{ data }}</div>`,