Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(useIDBKeyval): new integration - Idb-keyval wrapper #2335

Merged
merged 8 commits into from Nov 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -65,6 +65,7 @@
"eslint": "^8.25.0",
"esno": "^0.16.3",
"export-size": "^0.5.2",
"fake-indexeddb": "^4.0.0",
"fast-glob": "^3.2.12",
"firebase": "^9.11.0",
"fs-extra": "^10.1.0",
Expand Down
4 changes: 4 additions & 0 deletions packages/.test/polyfillIndexedDb.ts
@@ -0,0 +1,4 @@
// Supports polyfilling window, web workers, as well as node's global
import 'fake-indexeddb/auto'

export {}
1 change: 1 addition & 0 deletions packages/.test/setup.ts
@@ -1,6 +1,7 @@
import { Vue2, install, isVue2 } from 'vue-demi'
import './polyfillFetch'
import './polyfillPointerEvents'
import './polyfillIndexedDb'
import { beforeAll, beforeEach } from 'vitest'

const setupVueSwitch = () => {
Expand Down
1 change: 1 addition & 0 deletions packages/add-ons.md
Expand Up @@ -77,6 +77,7 @@ Integration wrappers for utility libraries
- [`useDrauu`](https://vueuse.org/integrations/useDrauu/) — reactive instance for [drauu](https://github.com/antfu/drauu)
- [`useFocusTrap`](https://vueuse.org/integrations/useFocusTrap/) — reactive wrapper for [`focus-trap`](https://github.com/focus-trap/focus-trap)
- [`useFuse`](https://vueuse.org/integrations/useFuse/) — easily implement fuzzy search using a composable with [Fuse.js](https://github.com/krisk/fuse)
- [`useIDBKeyval`](https://vueuse.org/integrations/useIDBKeyval/) — wrapper for [`idb-keyval`](https://www.npmjs.com/package/idb-keyval)
- [`useJwt`](https://vueuse.org/integrations/useJwt/) — wrapper for [`jwt-decode`](https://github.com/auth0/jwt-decode)
- [`useNProgress`](https://vueuse.org/integrations/useNProgress/) — reactive wrapper for [`nprogress`](https://github.com/rstacruz/nprogress)
- [`useQRCode`](https://vueuse.org/integrations/useQRCode/) — wrapper for [`qrcode`](https://github.com/soldair/node-qrcode)
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/README.md
Expand Up @@ -21,6 +21,7 @@ npm i <b>@vueuse/integrations</b>
- [`useDrauu`](https://vueuse.org/integrations/useDrauu/) — reactive instance for [drauu](https://github.com/antfu/drauu)
- [`useFocusTrap`](https://vueuse.org/integrations/useFocusTrap/) — reactive wrapper for [`focus-trap`](https://github.com/focus-trap/focus-trap)
- [`useFuse`](https://vueuse.org/integrations/useFuse/) — easily implement fuzzy search using a composable with [Fuse.js](https://github.com/krisk/fuse)
- [`useIDBKeyval`](https://vueuse.org/integrations/useIDBKeyval/) — wrapper for [`idb-keyval`](https://www.npmjs.com/package/idb-keyval)
- [`useJwt`](https://vueuse.org/integrations/useJwt/) — wrapper for [`jwt-decode`](https://github.com/auth0/jwt-decode)
- [`useNProgress`](https://vueuse.org/integrations/useNProgress/) — reactive wrapper for [`nprogress`](https://github.com/rstacruz/nprogress)
- [`useQRCode`](https://vueuse.org/integrations/useQRCode/) — wrapper for [`qrcode`](https://github.com/soldair/node-qrcode)
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/index.ts
Expand Up @@ -5,6 +5,7 @@ export * from './useCookies'
export * from './useDrauu'
export * from './useFocusTrap'
export * from './useFuse'
export * from './useIDBKeyval'
export * from './useJwt'
export * from './useNProgress'
export * from './useQRCode'
10 changes: 10 additions & 0 deletions packages/integrations/package.json
Expand Up @@ -86,6 +86,11 @@
"types": "./useAsyncValidator/component.d.ts",
"require": "./useAsyncValidator/component.cjs",
"import": "./useAsyncValidator/component.mjs"
},
"./useIDBKeyval": {
"types": "./useIDBKeyval.d.ts",
"require": "./useIDBKeyval.cjs",
"import": "./useIDBKeyval.mjs"
}
},
"main": "./index.cjs",
Expand All @@ -100,6 +105,7 @@
"drauu": "*",
"focus-trap": "*",
"fuse.js": "*",
"idb-keyval": "*",
"jwt-decode": "*",
"nprogress": "*",
"qrcode": "*",
Expand All @@ -124,6 +130,9 @@
"fuse.js": {
"optional": true
},
"idb-keyval": {
"optional": true
},
"jwt-decode": {
"optional": true
},
Expand Down Expand Up @@ -151,6 +160,7 @@
"drauu": "^0.3.2",
"focus-trap": "^7.0.0",
"fuse.js": "^6.6.2",
"idb-keyval": "^6.2.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also requires to be added to peer deps

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added to peer deps

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sxzz Regarding the first comment, thank you for your patience as I looked into this.
I looked through useLocalStorage and useSessionStorage to understand how they reuse useStorage. I believe you are suggesting that we could use indexDB storage in place of window?.localStorage, the problem I see is that useStorage converts all values to strings, and a big reason our team wanted to use indexDB is so we can store values other than just strings.
I also took a closer look at StorageLike, its get and set functions get and return strings, so I am not sure that using indexDB in this way would provide any gains to using localStorage as is.
Please let me know if I am misunderstanding something.

"jwt-decode": "^3.1.2",
"nprogress": "^0.2.0",
"qrcode": "^1.5.1",
Expand Down
42 changes: 42 additions & 0 deletions packages/integrations/useIDBKeyval/demo.vue
@@ -0,0 +1,42 @@
<script setup lang="ts">
import { stringify } from '@vueuse/docs-utils'
import { useIDBKeyval } from '@vueuse/integrations'

const KEY = 'vue-use-idb-keyval'

const stateObject = useIDBKeyval(`${KEY}-object`, {
name: 'Banana',
color: 'Yellow',
size: 'Medium',
count: 0,
})
const textObject = stringify(stateObject)

const stateString = useIDBKeyval(`${KEY}-string`, 'foobar')
const textString = stateString

const stateArray = useIDBKeyval(`${KEY}-array`, ['foo', 'bar', 'baz'])
const textArray = stringify(stateArray)
</script>

<template>
<h5>Object</h5>
<input v-model="stateObject.name" type="text">
<input v-model="stateObject.color" type="text">
<input v-model="stateObject.size" type="text">
<input v-model.number="stateObject.count" type="range" min="0" step="0.01" max="1000">

<pre lang="json">{{ textObject }}</pre>
<br>

<h5>String</h5>
<input v-model="stateString" type="text">
<pre>{{ textString }}</pre>
<br>

<h5>Array</h5>
<input v-model="stateArray[0]" type="text">
<input v-model="stateArray[1]" type="text">
<input v-model="stateArray[2]" type="text">
<pre lang="json">{{ textArray }}</pre>
</template>
35 changes: 35 additions & 0 deletions packages/integrations/useIDBKeyval/index.md
@@ -0,0 +1,35 @@
---
category: '@Integrations'
---

# useIDBKeyval

Wrapper for [`idb-keyval`](https://www.npmjs.com/package/idb-keyval).


## Install idb-keyval as a peer dependency

```bash
npm install idb-keyval
```

## Usage

```ts
import { useIDBKeyval } from '@vueuse/integrations'

// bind object
const storedObject = useIDBKeyval('my-idb-keyval-store', { hello: 'hi', greeting: 'Hello' })

// update object
storedObject.value.hello = 'hola'

// bind boolean
const flag = useIDBKeyval('my-flag', true) // returns Ref<boolean>

// bind number
const count = useIDBKeyval('my-count', 0) // returns Ref<number>

// delete data from idb storage
storedObject.value = null
```
74 changes: 74 additions & 0 deletions packages/integrations/useIDBKeyval/index.test.ts
@@ -0,0 +1,74 @@
import { nextTick } from 'vue-demi'
import { del, get, set, update } from 'idb-keyval'
import { useIDBKeyval } from '.'

const KEY = 'vue-use-idb-keyval'

const defaultState = {
name: 'Banana',
color: 'Yellow',
size: 'Medium',
count: 0,
}

beforeEach(() => {
vi.unmock('idb-keyval')
vi.mock('idb-keyval')
})

describe('useIDBKeyval', () => {
it('set/get', async () => {
const state = useIDBKeyval(KEY, { ...defaultState })
await nextTick()
expect(get).toHaveBeenCalled()
expect(set).toHaveBeenCalled()
expect(state.value).toEqual(defaultState)
})

it('update', async () => {
const state = useIDBKeyval(KEY, { ...defaultState })
state.value.name = 'Apple'
state.value.color = 'Red'
state.value.size = 'Giant'
state.value.count += 1

await nextTick()
expect(update).toHaveBeenCalled()
expect(state.value.name).toBe('Apple')
expect(state.value.color).toBe('Red')
expect(state.value.size).toBe('Giant')
expect(state.value.count).toBe(defaultState.count + 1)
})

it('del', async () => {
const state = useIDBKeyval(KEY, { ...defaultState })
state.value = null
await nextTick()
sxzz marked this conversation as resolved.
Show resolved Hide resolved
expect(del).toHaveBeenCalled()
})

it('string', async () => {
const state = useIDBKeyval(KEY, 'foo')
await nextTick()
expect(get).toHaveBeenCalled()
expect(set).toHaveBeenCalled()
expect(state.value).toEqual('foo')
state.value = 'bar'
await nextTick()
expect(state.value).toEqual('bar')
expect(update).toHaveBeenCalled()
})

it('array', async () => {
const defaultArray = ['foo', 'bar', 'baz']
const state = useIDBKeyval(KEY, [...defaultArray])
await nextTick()
expect(get).toHaveBeenCalled()
expect(set).toHaveBeenCalled()
expect(state.value).toEqual(defaultArray)
state.value[1] = 'boop'
await nextTick()
expect(state.value[1]).toEqual('boop')
expect(update).toHaveBeenCalled()
})
})
95 changes: 95 additions & 0 deletions packages/integrations/useIDBKeyval/index.ts
@@ -0,0 +1,95 @@
import type { ConfigurableFlush, MaybeComputedRef, RemovableRef } from '@vueuse/shared'
import { resolveUnref } from '@vueuse/shared'
import type { Ref } from 'vue-demi'
import { ref, shallowRef, watch } from 'vue-demi'
import { del, get, set, update } from 'idb-keyval'

export interface UseIDBOptions extends ConfigurableFlush {
/**
* Watch for deep changes
*
* @default true
*/
deep?: boolean

/**
* On error callback
*
* Default log error to `console.error`
*/
onError?: (error: unknown) => void

/**
* Use shallow ref as reference
*
* @default false
*/
shallow?: boolean
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this option and the associated code by having the user pass in a computed, ref, or shallowRef?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was following the same pattern as useStorage and useStorageAsync with the shallowRef here, but can change that if needed.

}

/**
*
* @param key
* @param initialValue
* @param options
*/
export function useIDBKeyval<T>(
key: IDBValidKey,
initialValue: MaybeComputedRef<T>,
options: UseIDBOptions = {},
): RemovableRef<T> {
const {
flush = 'pre',
deep = true,
shallow,
onError = (e) => {
console.error(e)
},
} = options

const data = (shallow ? shallowRef : ref)(initialValue) as Ref<T>

const rawInit: T = resolveUnref(initialValue)

async function read() {
try {
const rawValue = await get<T>(key)
if (rawValue === undefined) {
if (rawInit)
set(key, rawInit)
}
else {
data.value = rawValue
}
}
catch (e) {
onError(e)
}
}

read()

async function write() {
try {
if (data.value == null) {
await del(key)
}
else {
// IndexedDB does not support saving proxies, convert from proxy before saving
if (Array.isArray(data.value))
await update(key, () => (JSON.parse(JSON.stringify(data.value))))
else if (typeof data.value === 'object')
await update(key, () => ({ ...data.value }))
else
await update(key, () => (data.value))
}
}
catch (e) {
onError(e)
}
}

watch(data, () => write(), { flush, deep })

return data as RemovableRef<T>
}