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

feat: create @helia/remote-pinner library #2

Merged
merged 18 commits into from
Aug 29, 2023
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
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