From acd1699f3c5128fc206821f490a8e927f2e261ec Mon Sep 17 00:00:00 2001 From: Harmony Scarlet <55414687+Harmony222@users.noreply.github.com> Date: Tue, 8 Nov 2022 15:47:41 -0800 Subject: [PATCH] feat(useIDBKeyval): new integration - Idb-keyval wrapper (#2335) Co-authored-by: Jess Sachs closes https://github.com/vueuse/vueuse/issues/230 --- package.json | 1 + packages/.test/polyfillIndexedDb.ts | 4 + packages/.test/setup.ts | 1 + packages/add-ons.md | 1 + packages/integrations/README.md | 1 + packages/integrations/index.ts | 1 + packages/integrations/package.json | 10 ++ packages/integrations/useIDBKeyval/demo.vue | 42 ++++++++ packages/integrations/useIDBKeyval/index.md | 35 +++++++ .../integrations/useIDBKeyval/index.test.ts | 74 +++++++++++++++ packages/integrations/useIDBKeyval/index.ts | 95 +++++++++++++++++++ pnpm-lock.yaml | 74 +++++++++++++++ 12 files changed, 339 insertions(+) create mode 100644 packages/.test/polyfillIndexedDb.ts create mode 100644 packages/integrations/useIDBKeyval/demo.vue create mode 100644 packages/integrations/useIDBKeyval/index.md create mode 100644 packages/integrations/useIDBKeyval/index.test.ts create mode 100644 packages/integrations/useIDBKeyval/index.ts diff --git a/package.json b/package.json index 55d2f46aa5f..b8f18eab6a7 100644 --- a/package.json +++ b/package.json @@ -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.12.1", "fs-extra": "^10.1.0", diff --git a/packages/.test/polyfillIndexedDb.ts b/packages/.test/polyfillIndexedDb.ts new file mode 100644 index 00000000000..424b3ceed4e --- /dev/null +++ b/packages/.test/polyfillIndexedDb.ts @@ -0,0 +1,4 @@ +// Supports polyfilling window, web workers, as well as node's global +import 'fake-indexeddb/auto' + +export {} diff --git a/packages/.test/setup.ts b/packages/.test/setup.ts index 649688151c7..c2be2dc593c 100644 --- a/packages/.test/setup.ts +++ b/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 = () => { diff --git a/packages/add-ons.md b/packages/add-ons.md index e2a2b01ef03..be5f3349e52 100644 --- a/packages/add-ons.md +++ b/packages/add-ons.md @@ -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) diff --git a/packages/integrations/README.md b/packages/integrations/README.md index be16c0bd924..3aa79f27eab 100644 --- a/packages/integrations/README.md +++ b/packages/integrations/README.md @@ -21,6 +21,7 @@ npm i @vueuse/integrations - [`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) diff --git a/packages/integrations/index.ts b/packages/integrations/index.ts index 9e0711e2fb6..8b2cca5830f 100644 --- a/packages/integrations/index.ts +++ b/packages/integrations/index.ts @@ -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' diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 203c688df30..21b485fbe18 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -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", @@ -100,6 +105,7 @@ "drauu": "*", "focus-trap": "*", "fuse.js": "*", + "idb-keyval": "*", "jwt-decode": "*", "nprogress": "*", "qrcode": "*", @@ -124,6 +130,9 @@ "fuse.js": { "optional": true }, + "idb-keyval": { + "optional": true + }, "jwt-decode": { "optional": true }, @@ -151,6 +160,7 @@ "drauu": "^0.3.2", "focus-trap": "^7.0.0", "fuse.js": "^6.6.2", + "idb-keyval": "^6.2.0", "jwt-decode": "^3.1.2", "nprogress": "^0.2.0", "qrcode": "^1.5.1", diff --git a/packages/integrations/useIDBKeyval/demo.vue b/packages/integrations/useIDBKeyval/demo.vue new file mode 100644 index 00000000000..f5aa7455083 --- /dev/null +++ b/packages/integrations/useIDBKeyval/demo.vue @@ -0,0 +1,42 @@ + + + diff --git a/packages/integrations/useIDBKeyval/index.md b/packages/integrations/useIDBKeyval/index.md new file mode 100644 index 00000000000..4434eafea31 --- /dev/null +++ b/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 + +// bind number +const count = useIDBKeyval('my-count', 0) // returns Ref + +// delete data from idb storage +storedObject.value = null +``` diff --git a/packages/integrations/useIDBKeyval/index.test.ts b/packages/integrations/useIDBKeyval/index.test.ts new file mode 100644 index 00000000000..b803cfc5fb1 --- /dev/null +++ b/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() + 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() + }) +}) diff --git a/packages/integrations/useIDBKeyval/index.ts b/packages/integrations/useIDBKeyval/index.ts new file mode 100644 index 00000000000..2fec92bb148 --- /dev/null +++ b/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 +} + +/** + * + * @param key + * @param initialValue + * @param options + */ +export function useIDBKeyval( + key: IDBValidKey, + initialValue: MaybeComputedRef, + options: UseIDBOptions = {}, +): RemovableRef { + const { + flush = 'pre', + deep = true, + shallow, + onError = (e) => { + console.error(e) + }, + } = options + + const data = (shallow ? shallowRef : ref)(initialValue) as Ref + + const rawInit: T = resolveUnref(initialValue) + + async function read() { + try { + const rawValue = await get(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 +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc5352271b0..42042ae32a4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,7 @@ importers: 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.12.1 fs-extra: ^10.1.0 @@ -110,6 +111,7 @@ importers: 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.12.1 fs-extra: 10.1.0 @@ -202,6 +204,7 @@ importers: drauu: ^0.3.2 focus-trap: ^7.0.0 fuse.js: ^6.6.2 + idb-keyval: ^6.2.0 jwt-decode: ^3.1.2 nprogress: ^0.2.0 qrcode: ^1.5.1 @@ -220,6 +223,7 @@ importers: drauu: 0.3.2 focus-trap: 7.0.0 fuse.js: 6.6.2 + idb-keyval: 6.2.0 jwt-decode: 3.1.2 nprogress: 0.2.0 qrcode: 1.5.1 @@ -3552,6 +3556,11 @@ packages: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true + /base64-arraybuffer-es6/0.7.0: + resolution: {integrity: sha512-ESyU/U1CFZDJUdr+neHRhNozeCv72Y7Vm0m1DCbjX3KBjT6eYocvAJlSk6+8+HkVwXlT1FNxhGW6q3UKAlCvvw==} + engines: {node: '>=6.0.0'} + dev: true + /base64-js/1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} dev: true @@ -4306,6 +4315,12 @@ packages: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} dev: true + /domexception/1.0.1: + resolution: {integrity: sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==} + dependencies: + webidl-conversions: 4.0.2 + dev: true + /domexception/4.0.0: resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} engines: {node: '>=12'} @@ -5576,6 +5591,12 @@ packages: - supports-color dev: true + /fake-indexeddb/4.0.0: + resolution: {integrity: sha512-oCfWSJ/qvQn1XPZ8SHX6kY3zr1t+bN7faZ/lltGY0SBGhFOPXnWf0+pbO/MOAgfMx6khC2gK3S/bvAgQpuQHDQ==} + dependencies: + realistic-structured-clone: 3.0.0 + dev: true + /fast-deep-equal/3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true @@ -6221,6 +6242,12 @@ packages: safer-buffer: 2.1.2 dev: true + /idb-keyval/6.2.0: + resolution: {integrity: sha512-uw+MIyQn2jl3+hroD7hF8J7PUviBU7BPKWw4f/ISf32D4LoGu98yHjrzWWJDASu9QNrX10tCJqk9YY0ClWm8Ng==} + dependencies: + safari-14-idb-fix: 3.0.0 + dev: true + /idb/7.0.1: resolution: {integrity: sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==} dev: true @@ -7949,6 +7976,14 @@ packages: dependencies: picomatch: 2.3.1 + /realistic-structured-clone/3.0.0: + resolution: {integrity: sha512-rOjh4nuWkAqf9PWu6JVpOWD4ndI+JHfgiZeMmujYcPi+fvILUu7g6l26TC1K5aBIp34nV+jE1cDO75EKOfHC5Q==} + dependencies: + domexception: 1.0.1 + typeson: 6.1.0 + typeson-registry: 1.0.0-alpha.39 + dev: true + /regenerate-unicode-properties/10.0.1: resolution: {integrity: sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw==} engines: {node: '>=4'} @@ -8187,6 +8222,10 @@ packages: tslib: 2.4.0 dev: true + /safari-14-idb-fix/3.0.0: + resolution: {integrity: sha512-eBNFLob4PMq8JA1dGyFn6G97q3/WzNtFK4RnzT1fnLq+9RyrGknzYiM/9B12MnKAxuj1IXr7UKYtTNtjyKMBog==} + dev: true + /safe-buffer/5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -8835,6 +8874,13 @@ packages: punycode: 2.1.1 dev: true + /tr46/2.1.0: + resolution: {integrity: sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==} + engines: {node: '>=8'} + dependencies: + punycode: 2.1.1 + dev: true + /tr46/3.0.0: resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} engines: {node: '>=12'} @@ -8961,6 +9007,20 @@ packages: hasBin: true dev: true + /typeson-registry/1.0.0-alpha.39: + resolution: {integrity: sha512-NeGDEquhw+yfwNhguLPcZ9Oj0fzbADiX4R0WxvoY8nGhy98IbzQy1sezjoEFWOywOboj/DWehI+/aUlRVrJnnw==} + engines: {node: '>=10.0.0'} + dependencies: + base64-arraybuffer-es6: 0.7.0 + typeson: 6.1.0 + whatwg-url: 8.7.0 + dev: true + + /typeson/6.1.0: + resolution: {integrity: sha512-6FTtyGr8ldU0pfbvW/eOZrEtEkczHRUtduBnA90Jh9kMPCiFNnXIon3vF41N0S4tV1HHQt4Hk1j4srpESziCaA==} + engines: {node: '>=0.1.14'} + dev: true + /ufo/0.8.5: resolution: {integrity: sha512-e4+UtA5IRO+ha6hYklwj6r7BjiGMxS0O+UaSg9HbaTefg4kMkzj4tXzEBajRR+wkxf+golgAWKzLbytCUDMJAA==} @@ -9528,6 +9588,11 @@ packages: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} dev: true + /webidl-conversions/6.1.0: + resolution: {integrity: sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==} + engines: {node: '>=10.4'} + dev: true + /webidl-conversions/7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -9593,6 +9658,15 @@ packages: webidl-conversions: 4.0.2 dev: true + /whatwg-url/8.7.0: + resolution: {integrity: sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==} + engines: {node: '>=10'} + dependencies: + lodash: 4.17.21 + tr46: 2.1.0 + webidl-conversions: 6.1.0 + dev: true + /which-boxed-primitive/1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} dependencies: