Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/bundles/connected.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createSelector } from 'redux-bundler'
import { contextBridge } from '../helpers/context-bridge'

/**
* @typedef {Object} Model
Expand Down Expand Up @@ -80,6 +81,24 @@ const connected = {
return state
}
},

/**
* Bridge ipfsConnected state to context bridge for use by React contexts
*/
reactConnectedToBridge: createSelector(
'selectIpfsConnected',
(ipfsConnected) => {
contextBridge.setContext('selectIpfsConnected', ipfsConnected)
}
),

reactIsNodeInfoOpenToBridge: createSelector(
'selectIsNodeInfoOpen',
(isNodeInfoOpen) => {
contextBridge.setContext('selectIsNodeInfoOpen', isNodeInfoOpen)
}
),

...actions,
...selectors
}
Expand Down
34 changes: 0 additions & 34 deletions src/bundles/identity.js

This file was deleted.

2 changes: 0 additions & 2 deletions src/bundles/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import toursBundle from './tours.js'
import notifyBundle from './notify.js'
import connectedBundle from './connected.js'
import retryInitBundle from './retry-init.js'
import identityBundle from './identity.js'
import bundleCache from '../lib/bundle-cache.js'
import ipfsDesktop from './ipfs-desktop.js'
import repoStats from './repo-stats.js'
Expand All @@ -31,7 +30,6 @@ export default composeBundles(
}),
appIdle({ idleTimeout: 5000 }),
ipfsProvider,
identityBundle,
routesBundle,
redirectsBundle,
toursBundle,
Expand Down
20 changes: 19 additions & 1 deletion src/bundles/ipfs-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import last from 'it-last'
import * as Enum from '../lib/enum.js'
import { perform } from './task.js'
import { readSetting, writeSetting } from './local-storage.js'
import { contextBridge } from '../helpers/context-bridge'
import { createSelector } from 'redux-bundler'

/**
* @typedef {import('ipfs').IPFSService} IPFSService
Expand Down Expand Up @@ -305,7 +307,12 @@ const selectors = {
/**
* @param {State} state
*/
selectIpfsPendingFirstConnection: state => state.ipfs.pendingFirstConnection
selectIpfsPendingFirstConnection: state => state.ipfs.pendingFirstConnection,
/**
* Returns the IPFS instance. This is the same instance that getIpfs() returns.
* Used by the identity context to access IPFS directly.
*/
selectIpfs: () => ipfs
}

/**
Expand Down Expand Up @@ -524,6 +531,17 @@ const bundle = {
getExtraArgs () {
return extra
},

/**
* Bridge ipfs instance to context bridge for use by React contexts
*/
reactIpfsToBridge: createSelector(
'selectIpfs',
(ipfsInstance) => {
contextBridge.setContext('selectIpfs', ipfsInstance)
}
),

...selectors,
...actions
}
Expand Down
14 changes: 13 additions & 1 deletion src/bundles/peer-locations.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,22 @@ import { multiaddr } from '@multiformats/multiaddr'
import ms from 'milliseconds'
import ip from 'ip'
import memoize from 'p-memoize'
import { createContextSelector } from '../helpers/context-bridge'
import pkgJson from '../../package.json'

const { dependencies } = pkgJson

/**
* Selector that reads identity from the context bridge
* Returns the same format as the original selectIdentity for compatibility
*/
const selectIdentityFromContext = createContextSelector('identity')

const selectIdentityData = () => {
const identityContext = selectIdentityFromContext()
return identityContext?.identity
}

// After this time interval, we re-check the locations for each peer
// once again through PeerLocationResolver.
const UPDATE_EVERY = ms.seconds(1)
Expand Down Expand Up @@ -55,7 +67,7 @@ function createPeersLocations (opts) {
'selectPeers',
'selectPeerLocations',
'selectBootstrapPeers',
'selectIdentity', // ipfs.id info for local node, used for detecting local peers
selectIdentityData, // ipfs.id info from identity context, used for detecting local peers
(peers, locations = {}, bootstrapPeers, identity) => peers && Promise.all(peers.map(async (peer) => {
const peerId = peer.peer
const locationObj = locations ? locations[peerId] : null
Expand Down
199 changes: 199 additions & 0 deletions src/contexts/identity-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import React, { createContext, useContext, useReducer, useEffect, useCallback, useMemo, ReactNode, useRef } from 'react'
import { useBridgeContext, useBridgeSelector } from '../helpers/context-bridge'

/**
* Identity data structure
*/
export interface IdentityData {
/** The IPFS peer ID */
id?: string
/** The public key */
publicKey?: string
/** List of multiaddresses */
addresses?: string[]
/** Agent version */
agentVersion?: string
/** Protocol version */
protocolVersion?: string
}

/**
* Identity context value
*/
export interface IdentityContextValue {
/** The identity data, undefined if not loaded */
identity?: IdentityData
/** Whether identity is being fetched for the first time */
isLoading: boolean
/** Whether there was an error fetching identity */
hasError: boolean
/** Last successful fetch timestamp */
lastSuccess?: number
/** Function to manually refetch identity */
refetch: () => void
/**
* Whether identity is being updated (loading, but we already have a good identity response)
*/
isRefreshing: boolean
}

/**
* Identity state for the reducer
*/
interface IdentityState {
identity?: IdentityData
isLoading: boolean
hasError: boolean
lastSuccess?: number
}

/**
* Actions for the identity reducer
*/
type IdentityAction =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; payload: { identity: IdentityData; timestamp: number } }
| { type: 'FETCH_ERROR' }

/**
* Identity reducer
*/
function identityReducer (state: IdentityState, action: IdentityAction): IdentityState {
switch (action.type) {
case 'FETCH_START':
return {
...state,
isLoading: true,
hasError: false
}
case 'FETCH_SUCCESS':
return {
...state,
identity: action.payload.identity,
isLoading: false,
hasError: false,
lastSuccess: action.payload.timestamp
}
case 'FETCH_ERROR':
return {
...state,
isLoading: false,
hasError: true
}
default:
return state
}
}

/**
* Initial state
*/
const initialState: IdentityState = {
identity: undefined,
isLoading: false,
hasError: false,
lastSuccess: undefined
}

/**
* Identity context
*/
const IdentityContext = createContext<IdentityContextValue | undefined>(undefined)
IdentityContext.displayName = 'IdentityContext'

/**
* Identity Provider Props
*/
interface IdentityProviderProps {
children: ReactNode
}

/**
* Identity provider component using context bridge for redux selectors
*/
const IdentityProviderImpl: React.FC<IdentityProviderProps> = ({ children }) => {
const [state, dispatch] = useReducer(identityReducer, initialState)
const shouldPoll = useBridgeSelector<boolean>('selectIsNodeInfoOpen') || false
const ipfsConnected = useBridgeSelector<boolean>('selectIpfsConnected') || false
const ipfs = useBridgeSelector<any>('selectIpfs')

// keep last good identity to prevent UI flash
const lastGoodIdentityRef = useRef<IdentityData | undefined>(state.identity)
useEffect(() => {
if (state.identity) lastGoodIdentityRef.current = state.identity
}, [state.identity])

const identityStable = state.identity ?? lastGoodIdentityRef.current
const isInitialLoading = identityStable == null && state.isLoading
const isRefreshing = identityStable != null && state.isLoading

const fetchIdentity = useCallback(async () => {
if (!ipfsConnected || !ipfs) return
try {
dispatch({ type: 'FETCH_START' })
const identity = await ipfs.id()
dispatch({
type: 'FETCH_SUCCESS',
payload: { identity, timestamp: Date.now() }
})
} catch (error) {
console.error('Failed to fetch identity:', error)
dispatch({ type: 'FETCH_ERROR' })
}
}, [ipfs, ipfsConnected])

useEffect(() => {
if (ipfsConnected && !state.isLoading) {
if (!state.identity || !state.lastSuccess) {
fetchIdentity()
}
}
}, [ipfsConnected, fetchIdentity, state.isLoading, state.identity, state.lastSuccess])

useEffect(() => {
if (!shouldPoll || !ipfsConnected || !state.lastSuccess) return

const REFRESH_INTERVAL = 5000
const timeSinceLastSuccess = Date.now() - state.lastSuccess

if (timeSinceLastSuccess < REFRESH_INTERVAL) {
const timeout = setTimeout(fetchIdentity, REFRESH_INTERVAL - timeSinceLastSuccess)
return () => clearTimeout(timeout)
} else {
fetchIdentity()
}
}, [shouldPoll, ipfsConnected, state.lastSuccess, fetchIdentity])

const contextValue: IdentityContextValue = useMemo(() => ({
identity: identityStable,
isLoading: isInitialLoading,
isRefreshing,
hasError: state.hasError,
lastSuccess: state.lastSuccess,
refetch: fetchIdentity
}), [identityStable, isInitialLoading, isRefreshing, state.hasError, state.lastSuccess, fetchIdentity])

useBridgeContext('identity', contextValue)

return (
<IdentityContext.Provider value={contextValue}>
{children}
</IdentityContext.Provider>
)
}

/**
* Hook to consume the identity context
*/
export function useIdentity (): IdentityContextValue {
const context = useContext(IdentityContext)
if (context === undefined) {
throw new Error('useIdentity must be used within an IdentityProvider')
}
return context
}

/**
* Identity provider component
*/
export const IdentityProvider = IdentityProviderImpl
Loading