@helia/remote-pinning
Version:
Add remote pinning capabilities to Helia
211 lines • 7.48 kB
JavaScript
import { Status } from '@ipfs-shipyard/pinning-service-client';
import { NotFoundError, InvalidParametersError } from '@libp2p/interface';
import { logger } from '@libp2p/logger';
import { multiaddr } from '@multiformats/multiaddr';
import delay from 'delay';
import { CID } from 'multiformats/cid';
import { CustomProgressEvent } from 'progress-events';
import { raceSignal } from 'race-signal';
import { PinningFailedError } from "./errors.js";
const log = logger('helia:remote-pinning');
export class HeliaRemotePins {
helia;
remotePinningClient;
originFilter;
delegateFilter;
pollInterval;
constructor(helia, remotePinningClient, init = {}) {
this.helia = helia;
this.remotePinningClient = remotePinningClient;
this.originFilter = init.originFilter ?? ((arg) => arg);
this.delegateFilter = init.delegateFilter ?? ((arg) => arg);
this.pollInterval = init.pollInterval ?? 1000;
}
/**
* When starting a pinning operation the remote pinning service can send us a
* list of nodes to which it will delegate the fetching of data.
*
* We need to dial them for the pinning operation to complete.
*/
async connectToDelegates(delegates, options) {
log.trace('connect to %d delegates', delegates.length);
// for where we have been given multiple multiaddrs for each delegate, group
// them by embedded PeerId. Treat them individually if no PeerId is present.
const addresses = {};
this.delegateFilter(delegates).forEach(ma => {
const peerId = ma.getComponents().filter(c => c.name === 'p2p').map(c => c.value).pop() ?? `${Math.random()}`;
addresses[peerId] ??= [];
addresses[peerId].push(ma);
});
try {
await Promise.any(Object.values(addresses).map(async (addrs) => {
try {
await this.helia.libp2p.dial(addrs, options);
}
catch (err) {
log.error('failed to connect to delegate %s - %e', addrs, err);
throw err;
}
}));
}
catch (err) {
log.error('failed to connect to any delegates - %e', err);
}
}
#getOrigins(additionalOrigins = []) {
return this.originFilter([
...this.helia.libp2p.getMultiaddrs(),
...additionalOrigins
])
.map(ma => ma.toString());
}
async *add(cid, options = {}) {
const createResult = await this.remotePinningClient.pinsPost({
pin: {
...options,
cid: cid.toString(),
// @ts-expect-error - broken types: origins needs to be an array of strings
origins: this.#getOrigins(options.origins),
meta: options.metadata
}
}, options);
log.trace('initial pinsPost made, status: %s', createResult.status);
this.connectToDelegates(createResult.delegates.map(addr => multiaddr(addr)), options)
.catch(err => {
log.error('failed to connect to delegates - %e', err);
});
while (options.signal?.aborted !== true) {
const getResult = await this.remotePinningClient.pinsRequestidGet({
requestid: createResult.requestid
});
if (getResult.status === Status.Failed) {
throw new PinningFailedError(`Pinning ${cid} failed`);
}
if (getResult.status === Status.Pinned) {
break;
}
await raceSignal(delay(this.pollInterval), options.signal);
}
options.onProgress?.(new CustomProgressEvent('helia:pin:add', cid));
yield cid;
}
async *rm(cid, options = {}) {
// find the requestid for the pinned CID
const result = await this.remotePinningClient.pinsGet({
cid: [cid.toString()]
}, options);
// delete all requestids for the pinned CID
await Promise.all([...result.results].map(async (result) => {
return this.remotePinningClient.pinsRequestidDelete({
requestid: result.requestid
}, options);
}));
yield cid;
}
async *ls(options = {}) {
const request = {
...options,
cid: undefined,
limit: 1000
};
if (options.cid != null) {
request.cid = [options.cid.toString()];
}
try {
while (options?.signal?.aborted !== true) {
const page = await this.remotePinningClient.pinsGet(request, options);
if (page.results.length === 0) {
return;
}
yield* page.results.map(result => {
return {
cid: CID.parse(result.pin.cid),
depth: Infinity,
metadata: result.pin.meta ?? {},
name: result.pin.name,
status: result.status
};
});
request.after = page.results[page.results.length - 1].created;
}
}
catch (err) {
throw translateError(err);
}
}
async get(cid, options) {
const request = {
...options,
cid: [
cid.toString()
],
limit: 1
};
const page = await this.remotePinningClient.pinsGet(request, options);
if (page.results.length === 0) {
throw new NotFoundError();
}
const result = page.results[0];
return {
cid: CID.parse(result.pin.cid),
depth: Infinity,
metadata: result.pin.meta ?? {},
status: result.status
};
}
async setMetadata(cid, metadata, options) {
const request = {
...options,
cid: [
cid.toString()
],
limit: 1
};
const page = await this.remotePinningClient.pinsGet(request, options);
if (page.results.length === 0) {
throw new NotFoundError();
}
const result = page.results[0];
const updateRequest = {
requestid: result.requestid,
pin: {
...result.pin,
meta: metadata ?? {}
}
};
await this.remotePinningClient.pinsRequestidPost(updateRequest, options);
}
async isPinned(cid, options) {
try {
const page = await this.remotePinningClient.pinsGet({
...options,
cid: [cid.toString()],
limit: 1
}, options);
if (page.count === 0) {
return false;
}
return true;
}
catch (err) {
throw translateError(err);
}
}
}
/**
* The pinning service api client throws "Response" objects instead of "Error"s
* so translate them into a more palatable throwable
*/
function translateError(err) {
if (err instanceof Error) {
return err;
}
if (err.status === 404) {
return new NotFoundError();
}
if (err.status === 400) {
return new InvalidParametersError();
}
return new Error('Operation failed');
}
//# sourceMappingURL=helia-remote-pins.js.map