Skip to content

Commit

Permalink
feat: create @helia/remote-pinner library
Browse files Browse the repository at this point in the history
feat: create @helia/remote-pinner library
  • Loading branch information
SgtPooki authored Aug 29, 2023
2 parents 0d22511 + 52d998b commit 7ee93a7
Show file tree
Hide file tree
Showing 6 changed files with 412 additions and 3 deletions.
41 changes: 38 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,46 @@
$ npm i @helia/remote-pinning
```

A longer repository description.

## Documentation

[Insert link to documentation]() or expand with Install, Build, Usage sections.
### Create remote pinner

```typescript
import { unixfs } from '@helia/unixfs'
import { Configuration, RemotePinningServiceClient } from '@ipfs-shipyard/pinning-service-client'
import { createHelia } from 'helia'
import { createRemotePinner } from '@helia/remote-pinning'

const helia = await createHelia()
const pinServiceConfig = new Configuration({
endpointUrl: `${endpointUrl}`, // the URI for your pinning provider, e.g. `http://localhost:3000`
accessToken: `${accessToken}` // the secret token/key given to you by your pinning provider
})

const remotePinningClient = new RemotePinningServiceClient(pinServiceConfig)
const remotePinner = createRemotePinner(helia, remotePinningClient)
```

### Add a pin

```typescript
const heliaFs = unixfs(helia)
const cid = await heliaFs.addBytes(encoder.encode('hello world'))
const addPinResult = await remotePinner.addPin({
cid,
name: 'helloWorld'
})
```
### Replace a pin

```typescript
const newCid = await heliaFs.addBytes(encoder.encode('hi galaxy'))
const replacePinResult = await remotePinner.replacePin({
newCid,
name: 'hiGalaxy',
requestid: addPinResult.requestid
})
```

## Lead Maintainer

Expand Down
14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@
"sourceType": "module"
}
},
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"import": "./dist/src/index.js"
},
"./errors": {
"types": "./dist/src/errors.d.ts",
"import": "./dist/src/errors.js"
}
},
"release": {
"branches": [
"master"
Expand Down Expand Up @@ -117,6 +127,8 @@
},
"scripts": {
"clean": "aegir clean",
"lint": "aegir lint",
"lint:fix": "aegir lint -- --fix",
"test": "aegir test",
"test:chrome": "aegir test -t browser --cov",
"test:chrome-webworker": "aegir test -t webworker",
Expand All @@ -125,6 +137,7 @@
"test:node": "aegir test -t node --cov",
"test:electron-main": "aegir test -t electron-main",
"cov:report": "nyc report -t .coverage",
"prebuild": "npm run lint",
"build": "aegir build"
},
"devDependencies": {
Expand All @@ -143,6 +156,7 @@
"helia": "^1.3.11"
},
"dependencies": {
"@libp2p/logger": "^3.0.2",
"@multiformats/multiaddr": "^12.1.6",
"multiformats": "^12.0.1",
"p-retry": "^5.1.2"
Expand Down
10 changes: 10 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* when remote pinning service returns delegates, if we can't connect to any, we won't be able to provide our CID's
* content to the service, and must abort.
*/
export class FailedToConnectToDelegates extends Error {
constructor (message: string) {
super(message)
this.name = 'ERR_FAILED_TO_CONNECT_TO_DELEGATES'
}
}
133 changes: 133 additions & 0 deletions src/heliaRemotePinner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { type RemotePinningServiceClient, type Pin, type PinStatus, type PinsRequestidPostRequest, Status } from '@ipfs-shipyard/pinning-service-client'
import { logger } from '@libp2p/logger'
import { multiaddr } from '@multiformats/multiaddr'
import pRetry, { type Options as pRetryOptions } from 'p-retry'
import { FailedToConnectToDelegates } from './errors.js'
import type { Helia } from '@helia/interface'
import type { CID } from 'multiformats/cid'

const log = logger('helia:remote-pinning')

interface HeliaRemotePinningMethodOptions {
/**
* Control whether requests are aborted or not by manually aborting a signal or using AbortSignal.timeout()
*/
signal?: AbortSignal

/**
* The CID instance to pin. When using Helia, passing around the CID object is preferred over the string.
*/
cid: CID
}

export interface AddPinArgs extends Omit<Pin, 'cid'>, HeliaRemotePinningMethodOptions {}

export interface ReplacePinArgs extends Omit<PinsRequestidPostRequest, 'pin'>, Omit<Pin, 'cid'>, HeliaRemotePinningMethodOptions {}

export interface HeliaRemotePinnerConfig {
/**
* pRetry options when waiting for pinning to complete/fail in {@link handlePinStatus}
*
* @default { retries: 10 }
*/
retryOptions?: pRetryOptions
}

export class HeliaRemotePinner {
private readonly config: Required<HeliaRemotePinnerConfig>
constructor (private readonly heliaInstance: Helia, private readonly remotePinningClient: RemotePinningServiceClient, config?: HeliaRemotePinnerConfig) {
this.config = {
retryOptions: {
retries: 10,
...config?.retryOptions
}
}
}

private async getOrigins (otherOrigins: Pin['origins']): Promise<Set<string>> {
const origins = new Set(this.heliaInstance.libp2p.getMultiaddrs().map(multiaddr => multiaddr.toString()))
if (otherOrigins != null) {
for (const origin of otherOrigins) {
origins.add(origin)
}
}
return origins
}

private async connectToDelegates (delegates: Set<string>, signal?: AbortSignal): Promise<void> {
try {
await Promise.any([...delegates].map(async delegate => {
try {
await this.heliaInstance.libp2p.dial(multiaddr(delegate), { signal })
} catch (e) {
log.error(e)
throw e
}
}))
} catch (e) {
throw new FailedToConnectToDelegates('Failed to connect to any delegates')
}
}

/**
* The code that runs after we get a pinStatus from the remote pinning service.
* This method is the orchestrator for waiting for the pin to complete/fail as well as connecting to the delegates.
*/
private async handlePinStatus (pinStatus: PinStatus, signal?: AbortSignal): Promise<PinStatus> {
await this.connectToDelegates(pinStatus.delegates, signal)
let updatedPinStatus = pinStatus

/**
* We need to ensure that pinStatus is either pinned or failed.
* To do so, we will need to poll the remote pinning service for the status of the pin.
*/
try {
await pRetry(async (attemptNum) => {
log.trace('attempt #%d waiting for pinStatus of "pinned" or "failed"', attemptNum)
updatedPinStatus = await this.remotePinningClient.pinsRequestidGet({ requestid: pinStatus.requestid })
if ([Status.Pinned, Status.Failed].includes(pinStatus.status)) {
return updatedPinStatus
}
throw new Error(`Pin status is ${pinStatus.status}`)
}, {
signal,
...this.config?.retryOptions
})
} catch (e) {
log.error(e)
}

return updatedPinStatus
}

async addPin ({ cid, signal, ...otherArgs }: AddPinArgs): Promise<PinStatus> {
signal?.throwIfAborted()

const pinStatus = await this.remotePinningClient.pinsPost({
pin: {
...otherArgs,
cid: cid.toString(),
origins: await this.getOrigins(otherArgs.origins)
}
}, {
signal
})
return this.handlePinStatus(pinStatus, signal)
}

async replacePin ({ cid, requestid, signal, ...otherArgs }: ReplacePinArgs): Promise<PinStatus> {
signal?.throwIfAborted()

const pinStatus = await this.remotePinningClient.pinsRequestidPost({
requestid,
pin: {
...otherArgs,
cid: cid.toString(),
origins: await this.getOrigins(otherArgs.origins)
}
}, {
signal
})
return this.handlePinStatus(pinStatus, signal)
}
}
9 changes: 9 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { HeliaRemotePinner, type HeliaRemotePinnerConfig } from './heliaRemotePinner.js'
import type { Helia } from '@helia/interface'
import type { RemotePinningServiceClient } from '@ipfs-shipyard/pinning-service-client'

export type { HeliaRemotePinner, HeliaRemotePinnerConfig } from './heliaRemotePinner.js'

export function createRemotePinner (heliaInstance: Helia, remotePinningClient: RemotePinningServiceClient, config?: HeliaRemotePinnerConfig): HeliaRemotePinner {
return new HeliaRemotePinner(heliaInstance, remotePinningClient, config)
}
Loading

0 comments on commit 7ee93a7

Please sign in to comment.