-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: extract and test the datamodel logic from
Provider
(#602)
One of the core goals here was to fix a bug where the client wouldn't get reset when servicePrincipal or connection changed. The easiest way to make sure this was tested was to extract all of the datamodel creation and management logic to a dedicated hook and test that. Thanks to @gobengo for pointing out the bug and suggesting the fix: #595 (comment)
- Loading branch information
Showing
6 changed files
with
181 additions
and
50 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import type { | ||
Client, | ||
Space, | ||
Account, | ||
ServiceConfig | ||
} from '@w3ui/core' | ||
|
||
import { useState, useEffect, useCallback } from 'react' | ||
import { STORE_SAVE_EVENT, createClient } from '@w3ui/core' | ||
|
||
export type DatamodelProps = ServiceConfig | ||
|
||
export interface Datamodel { | ||
client?: Client | ||
accounts: Account[] | ||
spaces: Space[] | ||
logout: () => Promise<void> | ||
} | ||
|
||
export function useDatamodel ({ servicePrincipal, connection }: DatamodelProps): Datamodel { | ||
const [client, setClient] = useState<Client>() | ||
const [events, setEvents] = useState<EventTarget>() | ||
const [accounts, setAccounts] = useState<Account[]>([]) | ||
const [spaces, setSpaces] = useState<Space[]>([]) | ||
|
||
// update this function any time servicePrincipal or connection change | ||
const setupClient = useCallback( | ||
async (): Promise<void> => { | ||
const { client, events } = await createClient({ servicePrincipal, connection }) | ||
setClient(client) | ||
setEvents(events) | ||
setAccounts(Object.values(client.accounts())) | ||
setSpaces(client.spaces()) | ||
}, | ||
[servicePrincipal, connection] | ||
) | ||
|
||
// run setupClient once each time it changes | ||
useEffect( | ||
() => { | ||
void setupClient() | ||
}, | ||
[setupClient] | ||
) | ||
|
||
// set up event listeners to refresh accounts and spaces when | ||
// the store:save event from @w3ui/core happens | ||
useEffect(() => { | ||
if ((client === undefined) || (events === undefined)) return | ||
|
||
const handleStoreSave: () => void = () => { | ||
setAccounts(Object.values(client.accounts())) | ||
setSpaces(client.spaces()) | ||
} | ||
|
||
events.addEventListener(STORE_SAVE_EVENT, handleStoreSave) | ||
return () => { | ||
events?.removeEventListener(STORE_SAVE_EVENT, handleStoreSave) | ||
} | ||
}, [client, events]) | ||
|
||
const logout = async (): Promise<void> => { | ||
// it's possible that setupClient hasn't been run yet - run createClient here | ||
// to get a reliable handle on the latest store | ||
const { store } = await createClient({ servicePrincipal, connection }) | ||
await store.reset() | ||
// set state back to defaults | ||
setClient(undefined) | ||
setEvents(undefined) | ||
setAccounts([]) | ||
setSpaces([]) | ||
// set state up again | ||
await setupClient() | ||
} | ||
|
||
return { client, accounts, spaces, logout } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { test, expect } from 'vitest' | ||
import 'fake-indexeddb/auto' | ||
import { renderHook } from '@testing-library/react-hooks' | ||
import * as DID from '@ipld/dag-ucan/did' | ||
import { Principal, ConnectionView } from '@ucanto/interface' | ||
import { connect } from '@ucanto/client' | ||
import { CAR, HTTP } from '@ucanto/transport' | ||
|
||
import { useDatamodel } from '../src/hooks' | ||
|
||
test('should create a new client instance if and only if servicePrincipal or connection change', async () => { | ||
let servicePrincipal: Principal = DID.parse('did:web:web3.storage') | ||
let connection: ConnectionView<any> = connect({ | ||
id: servicePrincipal, | ||
codec: CAR.outbound, | ||
channel: HTTP.open<any>({ | ||
url: new URL('https://up.web3.storage'), | ||
method: 'POST' | ||
}) | ||
}) | ||
const { result, rerender, waitForValueToChange } = renderHook(() => useDatamodel({ servicePrincipal, connection })) | ||
// wait for client to be initialized | ||
await waitForValueToChange(() => result.current.client) | ||
|
||
const firstClient = result.current.client | ||
expect(firstClient).not.toBeFalsy() | ||
|
||
rerender() | ||
expect(result.current.client).toBe(firstClient) | ||
|
||
servicePrincipal = DID.parse('did:web:web3.porridge') | ||
rerender() | ||
// wait for the client to change | ||
await waitForValueToChange(() => result.current.client) | ||
// this is a little superfluous - if it's false then the line before this will hang | ||
// I still think it's worth keeping to illustrate the point | ||
expect(result.current.client).not.toBe(firstClient) | ||
const secondClient = result.current.client | ||
|
||
connection = connect({ | ||
id: servicePrincipal, | ||
codec: CAR.outbound, | ||
channel: HTTP.open<any>({ | ||
url: new URL('https://up.web3.porridge'), | ||
method: 'POST' | ||
}) | ||
}) | ||
rerender() | ||
await waitForValueToChange(() => result.current.client) | ||
expect(result.current.client).not.toBe(firstClient) | ||
expect(result.current.client).not.toBe(secondClient) | ||
}) |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.