Skip to content

Commit

Permalink
feat: allow async storage
Browse files Browse the repository at this point in the history
  • Loading branch information
tmm committed Sep 9, 2023
1 parent d32a8e4 commit c8ea691
Show file tree
Hide file tree
Showing 15 changed files with 156 additions and 86 deletions.
26 changes: 20 additions & 6 deletions docs/shared/createStorage.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,29 @@ If you use a custom `serialize` function, make sure it can handle `bigint` and `

### storage

`{ getItem: (key: string) => string | null; setItem: (key: string, value: string) => void; removeItem: (key: string) => void; }`
`{ getItem(key: string): string | null | undefined | Promise<string | null | undefined>; setItem(key: string, value: string): void | Promise<void>; removeItem(key: string): void | Promise<void>; }`

Storage interface to use for persisting data.
- Storage interface to use for persisting data.
- Defaults to `localStorage`.
- Supports synchronous and asynchronous storage methods.

```ts-vue
import { createStorage } from '{{packageName}}'
// Using IndexedDB via https://github.com/jakearchibald/idb-keyval // [!code focus]
import { del, get, set } from 'idb-keyval' // [!code focus]
const storage = createStorage({
storage: localStorage, // [!code focus]
storage: { // [!code focus]
async getItem(name) { // [!code focus]
return get(name)// [!code focus]
}, // [!code focus]
async setItem(name, value) { // [!code focus]
await set(name, value) // [!code focus]
}, // [!code focus]
async removeItem(name) { // [!code focus]
await del(name) // [!code focus]
}, // [!code focus]
}, // [!code focus]
})
```

Expand All @@ -115,7 +129,7 @@ import { type Storage } from '{{packageName}}'

### getItem

`getItem(key: string, defaultValue?: value | null) => value | null`
`getItem(key: string, defaultValue?: value | null | undefined): value | null | Promise<value | null>`

```ts-vue
import { createStorage } from '{{packageName}}'
Expand All @@ -126,7 +140,7 @@ const recentConnectorId = storage.getItem('recentConnectorId') // [!code focus]

### setItem

`setItem(key: string, value: any) => void`
`setItem(key: string, value: any): void | Promise<void>`

```ts-vue
import { createStorage } from '{{packageName}}'
Expand All @@ -137,7 +151,7 @@ storage.setItem('recentConnectorId', 'foo') // [!code focus]

### removeItem

`removeItem(key: string) => void`
`removeItem(key: string): void | Promise<void>`

```ts-vue
import { createStorage } from '{{packageName}}'
Expand Down
10 changes: 5 additions & 5 deletions packages/connectors/src/injected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export function injected(parameters: InjectedParameters = {}) {
)
const isDisconnected =
shimDisconnect &&
!config.storage?.getItem(this.shimDisconnectStorageKey)
!(await config.storage?.getItem(this.shimDisconnectStorageKey))

let accounts: readonly Address[] | null = null
if (canSelectAccount && isDisconnected) {
Expand Down Expand Up @@ -200,7 +200,7 @@ export function injected(parameters: InjectedParameters = {}) {

// Add shim to storage signalling wallet is connected
if (shimDisconnect)
config.storage?.setItem(this.shimDisconnectStorageKey, true)
await config.storage?.setItem(this.shimDisconnectStorageKey, true)

return { accounts, chainId: currentChainId }
} catch (error) {
Expand All @@ -225,7 +225,7 @@ export function injected(parameters: InjectedParameters = {}) {

// Remove shim signalling wallet is disconnected
if (shimDisconnect)
config.storage?.removeItem(this.shimDisconnectStorageKey)
await config.storage?.removeItem(this.shimDisconnectStorageKey)
},
async getAccounts() {
const provider = await this.getProvider()
Expand All @@ -251,7 +251,7 @@ export function injected(parameters: InjectedParameters = {}) {
const isDisconnected =
shimDisconnect &&
// If shim does not exist in storage, wallet is disconnected
!config.storage?.getItem(this.shimDisconnectStorageKey)
!(await config.storage?.getItem(this.shimDisconnectStorageKey))
if (isDisconnected) return false

const provider = await this.getProvider()
Expand Down Expand Up @@ -406,7 +406,7 @@ export function injected(parameters: InjectedParameters = {}) {

// Add shim to storage signalling wallet is connected
if (shimDisconnect)
config.storage?.setItem(this.shimDisconnectStorageKey, true)
await config.storage?.setItem(this.shimDisconnectStorageKey, true)
},
async onDisconnect(error) {
const provider = await this.getProvider()
Expand Down
11 changes: 5 additions & 6 deletions packages/connectors/src/safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function safe(parameters: SafeParameters = {}) {

// Add shim to storage signalling wallet is connected
if (parameters.shimDisconnect)
config.storage?.setItem(shimDisconnectStorageKey, true)
await config.storage?.setItem(shimDisconnectStorageKey, true)

return { accounts, chainId }
},
Expand All @@ -65,7 +65,7 @@ export function safe(parameters: SafeParameters = {}) {

// Remove shim signalling wallet is disconnected
if (parameters.shimDisconnect)
config.storage?.removeItem(shimDisconnectStorageKey)
await config.storage?.removeItem(shimDisconnectStorageKey)
},
async getAccounts() {
const provider = await this.getProvider()
Expand All @@ -89,12 +89,11 @@ export function safe(parameters: SafeParameters = {}) {
},
async isAuthorized() {
try {
if (
const isDisconnected =
parameters.shimDisconnect &&
// If shim does not exist in storage, wallet is disconnected
!config.storage?.getItem(shimDisconnectStorageKey)
)
return false
!(await config.storage?.getItem(shimDisconnectStorageKey))
if (isDisconnected) return false

const accounts = await this.getAccounts()
return !!accounts.length
Expand Down
26 changes: 14 additions & 12 deletions packages/connectors/src/walletConnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ export function walletConnect(parameters: WalletConnectParameters) {
}>
getNamespaceChainsIds(): number[]
getNamespaceMethods(): NamespaceMethods[]
getRequestedChainsIds(): number[]
isChainsStale(): boolean
getRequestedChainsIds(): Promise<number[]>
isChainsStale(): Promise<boolean>
onConnect(connectInfo: ProviderConnectInfo): void
onDisplayUri(uri: string): void
onSessionDelete(data: { topic: string }): void
Expand Down Expand Up @@ -118,7 +118,7 @@ export function walletConnect(parameters: WalletConnectParameters) {

let targetChainId = chainId
if (!targetChainId) {
const state = config.storage?.getItem('state') ?? {}
const state = (await config.storage?.getItem('state')) ?? {}
const isChainSupported = config.chains.some(
(x) => x.id === state.chainId,
)
Expand All @@ -127,7 +127,7 @@ export function walletConnect(parameters: WalletConnectParameters) {
}
if (!targetChainId) throw new Error('No chains found on connector.')

const isChainsStale = this.isChainsStale()
const isChainsStale = await this.isChainsStale()
// If there is an active session with stale chains, disconnect current session.
if (provider.session && isChainsStale) await provider.disconnect()

Expand Down Expand Up @@ -240,7 +240,7 @@ export function walletConnect(parameters: WalletConnectParameters) {
if (!accounts.length) return false

// If the chains are stale on the session, then the connector is unauthorized.
const isChainsStale = this.isChainsStale()
const isChainsStale = await this.isChainsStale()
if (isChainsStale && provider.session) {
await provider.disconnect().catch(() => {})
return false
Expand Down Expand Up @@ -276,7 +276,7 @@ export function walletConnect(parameters: WalletConnectParameters) {
},
],
})
const requestedChains = this.getRequestedChainsIds()
const requestedChains = await this.getRequestedChainsIds()
this.setRequestedChainsIds([...requestedChains, chainId])
}

Expand Down Expand Up @@ -341,8 +341,10 @@ export function walletConnect(parameters: WalletConnectParameters) {
?.methods as NamespaceMethods[]
return methods ?? []
},
getRequestedChainsIds() {
return config.storage?.getItem(this.requestedChainsStorageKey) ?? []
async getRequestedChainsIds() {
return (
(await config.storage?.getItem(this.requestedChainsStorageKey)) ?? []
)
},
/**
* Checks if the target chains match the chains that were
Expand All @@ -366,7 +368,7 @@ export function walletConnect(parameters: WalletConnectParameters) {
*
* Also check that dapp supports at least 1 chain from previously approved session.
*/
isChainsStale() {
async isChainsStale() {
const namespaceMethods = this.getNamespaceMethods()
if (namespaceMethods.includes('wallet_addEthereumChain')) return false
if (!isNewChainsStale) return false
Expand All @@ -379,11 +381,11 @@ export function walletConnect(parameters: WalletConnectParameters) {
)
return false

const requestedChains = this.getRequestedChainsIds()
const requestedChains = await this.getRequestedChainsIds()
return !connectorChains.every((id) => requestedChains.includes(id))
},
setRequestedChainsIds(chains) {
config.storage?.setItem(this.requestedChainsStorageKey, chains)
async setRequestedChainsIds(chains) {
await config.storage?.setItem(this.requestedChainsStorageKey, chains)
},
get requestedChainsStorageKey() {
return `${this.id}.requestedChains` as Properties['requestedChainsStorageKey']
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/actions/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export async function connect<config extends Config>(
connector.emitter.on('change', config._internal.change)
connector.emitter.on('disconnect', config._internal.disconnect)

config.storage?.setItem('recentConnectorId', connector.id)
await config.storage?.setItem('recentConnectorId', connector.id)
config.setState((x) => ({
...x,
connections: new Map(x.connections).set(connector.uid, {
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/actions/disconnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,13 @@ export async function disconnect(
current: nextConnection.connector.uid,
}
})

// Set recent connector if exists
{
const current = config.state.current
if (!current) return
const connector = config.state.connections.get(current)?.connector
if (!connector) return
await config.storage?.setItem('recentConnectorId', connector.id)
}
}
2 changes: 1 addition & 1 deletion packages/core/src/actions/reconnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export async function reconnect(
} else connectors.push(...config.connectors)

// Try recently-used connectors first
const recentConnectorId = config.storage?.getItem('recentConnectorId')
const recentConnectorId = await config.storage?.getItem('recentConnectorId')
const scores: Record<string, number> = {}
for (const [, connection] of config.state.connections) {
scores[connection.connector.id] = 1
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/actions/switchAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export async function switchAccount<config extends Config>(
const connection = config.state.connections.get(connector.uid)
if (!connection) throw new ConnectorNotConnectedError()

config.storage?.setItem('recentConnectorId', connector.id)
await config.storage?.setItem('recentConnectorId', connector.id)
config.setState((x) => ({
...x,
current: connector.uid,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/createConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { Emitter, type EventData, createEmitter } from './createEmitter.js'
import { type Storage, createStorage, noopStorage } from './createStorage.js'
import { ChainNotConfiguredError } from './errors/config.js'
import type { MultichainValue } from './types/chain.js'
import type { Evaluate, OneOf } from './types/utils.js'
import type { Evaluate, ExactPartial, OneOf } from './types/utils.js'
import { uid } from './utils/uid.js'

export type CreateConfigParameters<
Expand Down Expand Up @@ -358,7 +358,7 @@ export type State<
status: 'connected' | 'connecting' | 'disconnected' | 'reconnecting'
}
export type PartializedState = Evaluate<
Partial<Pick<State, 'chainId' | 'connections' | 'current' | 'status'>>
ExactPartial<Pick<State, 'chainId' | 'connections' | 'current' | 'status'>>
>
export type Connection = {
accounts: readonly [Address, ...Address[]]
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/createConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export type CreateConnectorFn<
> = (config: {
chains: readonly [Chain, ...Chain[]]
emitter: Emitter<ConnectorEventMap>
storage?: Storage<storageItem> | null | undefined
storage?: Evaluate<Storage<storageItem>> | null | undefined
}) => Evaluate<
{
readonly id: string
Expand Down
53 changes: 39 additions & 14 deletions packages/core/src/createStorage.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,60 @@ import { createStorage } from './createStorage.js'

import type { Connection } from './createConfig.js'

const storage = createStorage({ storage: localStorage })

test('getItem', () => {
const storage = createStorage({ storage: localStorage })

expectTypeOf(storage.getItem('recentConnectorId')).toEqualTypeOf<
string | null
string | null | Promise<string | null>
>()
expectTypeOf(storage.getItem('recentConnectorId', 'foo')).toEqualTypeOf<
string | Promise<string>
>()
expectTypeOf(
storage.getItem('recentConnectorId', 'foo'),
).toEqualTypeOf<string>()
// @ts-expect-error unknown key
storage.getItem('foo')
// @ts-expect-error incorrect argument type
storage.getItem('recentConnectorId', 1n)

expectTypeOf(storage.getItem('state')).toEqualTypeOf<{
chainId?: number
connections?: Map<string, Connection>
current?: string | undefined
status?: 'connected' | 'connecting' | 'reconnecting' | 'disconnected'
} | null>()
expectTypeOf(storage.getItem('state')).toEqualTypeOf<
| {
chainId?: number | undefined
connections?: Map<string, Connection> | undefined
current?: string | undefined
status?:
| 'connected'
| 'connecting'
| 'reconnecting'
| 'disconnected'
| undefined
}
| null
| Promise<{
chainId?: number | undefined
connections?: Map<string, Connection> | undefined
current?: string | undefined
status?:
| 'connected'
| 'connecting'
| 'reconnecting'
| 'disconnected'
| undefined
} | null>
>()

const customStorage = createStorage<{ foo: number }>({
storage: localStorage,
})
expectTypeOf(customStorage.getItem('foo')).toEqualTypeOf<number | null>()
expectTypeOf(customStorage.getItem('foo', 1)).toEqualTypeOf<number>()
expectTypeOf(customStorage.getItem('foo')).toEqualTypeOf<
number | null | Promise<number | null>
>()
expectTypeOf(customStorage.getItem('foo', 1)).toEqualTypeOf<
number | Promise<number>
>()
})

test('setItem', () => {
const storage = createStorage({ storage: localStorage })

storage.setItem('recentConnectorId', 'foo')
// @ts-expect-error incorrect argument type
storage.setItem('recentConnectorId', 1n)
Expand Down
Loading

1 comment on commit c8ea691

@vercel
Copy link

@vercel vercel bot commented on c8ea691 Sep 9, 2023

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

wagmi-v2 – ./docs

wagmi-v2.vercel.app
wagmi-v2-git-alpha-wagmi-dev.vercel.app
alpha.wagmi.sh
wagmi-v2-wagmi-dev.vercel.app

Please sign in to comment.