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

IPFS nodes healthcheck and API client #618

Closed
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
"@types/dompurify": "^3.0.5",
"@types/emoji-mart": "^3.0.14",
"@types/eslint": "^8.56.6",
"@types/lodash": "^4.17.0",
"@types/pbkdf2": "^3.1.2",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^7.3.1",
Expand Down
11 changes: 7 additions & 4 deletions src/components/AChat/AChatForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,8 @@ export default {
if (this.message.startsWith('/')) {
this.$store.commit('botCommands/addCommand', {
partnerId: this.partnerId,
command: this.message
command: this.message.trim(),
timestamp: Date.now()
})
}
this.$emit('message', this.message)
Expand All @@ -230,6 +231,8 @@ export default {
message: this.message,
partnerId: this.partnerId
})
this.botCommandIndex = null
this.botCommandSelectionMode = false
} else {
this.$emit('error', error)
}
Expand Down Expand Up @@ -259,22 +262,22 @@ export default {
if (this.botCommandIndex === null) {
if (direction === 'ArrowUp') {
this.botCommandIndex = maxIndex
this.message = commands[this.botCommandIndex] || ''
this.message = commands[this.botCommandIndex]?.command || ''
}
return
}

if (direction === 'ArrowUp') {
if (this.botCommandIndex > 0) {
this.botCommandIndex--
this.message = commands[this.botCommandIndex] || ''
this.message = commands[this.botCommandIndex]?.command || ''
}
return
}

if (this.botCommandIndex < maxIndex) {
this.botCommandIndex++
this.message = commands[this.botCommandIndex] || ''
this.message = commands[this.botCommandIndex]?.command || ''
}
}
}
Expand Down
48 changes: 48 additions & 0 deletions src/components/nodes/ipfs/IpfsNodesTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<template>
<NodesTableContainer>
<NodesTableHead hide-label />

<tbody>
<IpfsNodesTableItem v-for="node in ipfsNodes" :key="node.url" blockchain="adm" :node="node" />
</tbody>
</NodesTableContainer>
</template>

<script lang="ts">
import { computed, defineComponent } from 'vue'
import { useStore } from 'vuex'
import NodesTableContainer from '@/components/nodes/components/NodesTableContainer.vue'
import NodesTableHead from '@/components/nodes/components/NodesTableHead.vue'
import IpfsNodesTableItem from './IpfsNodesTableItem.vue'
import { sortNodesFn } from '@/components/nodes/utils/sortNodesFn'

const className = 'adm-nodes-table'
Copy link
Member Author

Choose a reason for hiding this comment

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

Suggested change
const className = 'adm-nodes-table'
const className = 'ipfs-nodes-table'

Copy link
Member Author

Choose a reason for hiding this comment

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

Also add this selector in <style> block. Leave empty if no styles

const classes = {
root: className
}

export default defineComponent({
components: {
NodesTableContainer,
NodesTableHead,
IpfsNodesTableItem
},
setup() {
const store = useStore()
const ipfsNodes = computed(() => {
const arr = store.getters['nodes/ipfs']

return [...arr].sort(sortNodesFn)
})

return {
ipfsNodes,
classes
}
}
})
</script>

<style lang="scss">
@import 'vuetify/settings';
</style>
122 changes: 122 additions & 0 deletions src/components/nodes/ipfs/IpfsNodesTableItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<template>
<tr :class="classes.root">
<NodeColumn checkbox>
<NodeStatusCheckbox :value="active" @change="toggleActiveStatus" />
</NodeColumn>

<NodeColumn>
<NodeUrl :node="node" />
<NodeVersion v-if="node.version && active" :node="node" />
</NodeColumn>

<NodeColumn :colspan="!showSocketColumn ? 2 : 1">
<NodeStatus :node="node" />
</NodeColumn>

<NodeColumn v-if="showSocketColumn">
<SocketSupport :node="node" />
</NodeColumn>
</tr>
</template>

<script lang="ts">
import { computed, PropType } from 'vue'
import { useStore } from 'vuex'
import type { NodeStatusResult } from '@/lib/nodes/abstract.node'
import NodeUrl from '@/components/nodes/components/NodeUrl.vue'
import NodeColumn from '@/components/nodes/components/NodeColumn.vue'
import NodeStatus from '@/components/nodes/components/NodeStatus.vue'
import NodeVersion from '@/components/nodes/components/NodeVersion.vue'
import SocketSupport from '@/components/nodes/components/SocketSupport.vue'
import NodeStatusCheckbox from '@/components/nodes/components/NodeStatusCheckbox.vue'

const className = 'adm-nodes-table-item'
const classes = {
root: className,
column: `${className}__column`,
columnCheckbox: `${className}__column--checkbox`,
checkbox: `${className}__checkbox`
}

export default {
components: {
NodeStatusCheckbox,
NodeColumn,
NodeStatus,
NodeVersion,
SocketSupport,
NodeUrl
},
props: {
node: {
type: Object as PropType<NodeStatusResult>,
required: true
}
},
setup(props) {
const store = useStore()

const url = computed(() => props.node.url)
const active = computed(() => props.node.active)
const socketSupport = computed(() => props.node.socketSupport)
const isUnsupported = computed(() => props.node.status === 'unsupported_version')
const type = computed(() => props.node.type)
const showSocketColumn = computed(() => active.value && !isUnsupported.value)

const toggleActiveStatus = () => {
store.dispatch('nodes/toggle', {
type: type.value,
url: url.value,
active: !active.value
})
store.dispatch('nodes/updateStatus')
}

const computedResult = computed(() => {
const baseUrl = new URL(url.value)
const protocol = baseUrl.protocol
const hostname = baseUrl.hostname
const port = baseUrl.port
const result = /^[\d.]+$/.test(hostname)

let nodeName = null
let domain = null

if (!result) {
const regex = /([^.]*)\.(.*)/
const parts = hostname.match(regex)
if (parts !== null) {
nodeName = parts[1]
domain = parts[2]
}
}

return {
protocol,
hostname,
nodeName,
domain,
result,
port
}
})
Comment on lines +75 to +102
Copy link
Member Author

Choose a reason for hiding this comment

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

Can you convert this part into a hook and reuse it in both components: IpfsNodesTableItem.vue and AdmNodesTableItem.vue?

Example of usage:

const { protocol, hostname, nodeName, domain, result, port } = useNodeUrl(url);

Put the hook in src/components/nodes/hooks

Copy link
Member Author

@bludnic bludnic Apr 28, 2024

Choose a reason for hiding this comment

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

It's important not to lose the reactivity of protocol, hostname, etc., after destructuring. You probably need to use toRefs inside the hook.


return {
classes,
url,
active,
socketSupport,
isUnsupported,
showSocketColumn,
toggleActiveStatus,
computedResult
}
}
}
</script>

<style lang="scss">
.ipfs-nodes-table-item {
line-height: 14px;
}
</style>
1 change: 1 addition & 0 deletions src/components/nodes/ipfs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as IpfsNodesTable } from './IpfsNodesTable.vue'
2 changes: 2 additions & 0 deletions src/lib/nodes/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type TNodeLabel =
| 'doge-node'
| 'doge-indexer'
| 'dash-node'
| 'ipfs-node'
| 'lsk-node'
| 'lsk-indexer'
| 'rates-info'
Expand Down Expand Up @@ -36,6 +37,7 @@ export const NODE_LABELS: NodeLabels = {
DogeNode: 'doge-node',
DogeIndexer: 'doge-indexer',
DashNode: 'dash-node',
IpfsNode: 'ipfs-node',
LskNode: 'lsk-node',
LskIndexer: 'lsk-indexer',
RatesInfo: 'rates-info'
Expand Down
62 changes: 62 additions & 0 deletions src/lib/nodes/ipfs/IpfsClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { isNodeOfflineError } from '@/lib/nodes/utils/errors'
import { IpfsNode, Payload, RequestConfig } from './IpfsNode.ts'
import { Client } from '../abstract.client'

/**
* Provides methods for calling the ADAMANT API.
*
* The `ApiClient` instance automatically selects an ADAMANT node to
* send the API-requests to and switches to another node if the current one
* is not available at the moment.
*/
Comment on lines +5 to +11
Copy link
Member Author

Choose a reason for hiding this comment

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

Update JSDoc: ADAMANT API -> IPFS NODE API

export class IpfsClient extends Client<IpfsNode> {
constructor(endpoints: string[] = [], minNodeVersion = '0.0.0') {
super('ipfs')
this.nodes = endpoints.map((endpoint) => new IpfsNode(endpoint, minNodeVersion))
this.minNodeVersion = minNodeVersion

void this.watchNodeStatusChange()
}

/**
* Performs a GET API request.
* @param {String} url relative API url
* @param {any} params request params (an object) or a function that accepts `ApiNode` and returns the request params
*/
get<P extends Payload = Payload>(url: string, params: P) {
return this.request({ method: 'get', url, payload: params })
}

/**
* Performs a POST API request.
* @param {String} url relative API url
* @param {any} payload request payload (an object) or a function that accepts `ApiNode` and returns the request payload
*/
post<P extends Payload = Payload>(url: string, payload: P) {
return this.request({ method: 'post', url, payload })
}

/**
* Performs an API request.
* @param {RequestConfig} config request config
*/
async request<P extends Payload = Payload, R = any>(config: RequestConfig<P>): Promise<R> {
const node = this.useFastest ? this.getFastestNode() : this.getRandomNode()
if (!node) {
// All nodes seem to be offline: let's refresh the statuses
this.checkHealth()
// But there's nothing we can do right now
return Promise.reject(new Error('No online nodes at the moment'))
}

return node.request(config).catch((error) => {
if (isNodeOfflineError(error)) {
// Initiate nodes status check
this.checkHealth()
// If the selected node is not available, repeat the request with another one.
return this.request(config)
}
throw error
})
}
}
Loading
Loading