Skip to content

Commit

Permalink
feat(@ew-did-registry/did-ipfs-store): is pinned when fully replicated
Browse files Browse the repository at this point in the history
JGiter committed Oct 19, 2022
1 parent 74118b0 commit ccef0e6
Showing 7 changed files with 124 additions and 65 deletions.
52 changes: 12 additions & 40 deletions packages/did-ipfs-store/package-lock.json

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

3 changes: 1 addition & 2 deletions packages/did-ipfs-store/package.json
Original file line number Diff line number Diff line change
@@ -30,8 +30,7 @@
"@web-std/fetch": "^4.1.0",
"@web-std/file": "^3.0.2",
"@web-std/form-data": "^3.0.2",
"axios": "^0.27.2",
"p-wait-for": "^5.0.0"
"axios": "^0.27.2"
},
"devDependencies": {
"@types/node": "^15.12.3",
75 changes: 53 additions & 22 deletions packages/did-ipfs-store/src/didStore.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { IDidStore } from '@ew-did-registry/did-store-interface';
import fetch from '@web-std/fetch';
import { FormData } from '@web-std/form-data';
import { File, Blob } from '@web-std/file';
import { Blob } from '@web-std/file';
import axios from 'axios';
import { values } from 'lodash';
import { StatusResponse, TrackerStatus } from './ipfs.types';

Object.assign(global, { fetch, File, Blob, FormData });
import {
AddResponse,
PinResponse,
StatusResponse,
TrackerStatus,
PIN_TIMEOUT,
REPLICATION,
} from './ipfs.types';
import { waitFor } from './utils';

/**
* Implements decentralized storage in IPFS cluster. Storing data in cluster allows to provide required degree of data availability
@@ -29,21 +35,22 @@ export class DidStore implements IDidStore {

/**
* @param claim stringified claim. Supported types of claim content are `string` and `object`
* @param pinTimeout Defines how to long to wait for pinning completion before throwing error, seconds
* @param waitForPinned checker of claim availability. Replication of claim requires time. This parameter allows to configure degree of availability after `save` returns. By default `save` waits until claim is pinned on `replication_factor_min` nodes
*/
async save(claim: string, pinTimeout = 5): Promise<string> {
async save(
claim: string,
waitForPinned?: (cid: string) => Promise<void>
): Promise<string> {
const blob = new Blob([claim]);
const { cid } = await this.add(blob);

const { default: waitFor } = await (eval('import(`p-wait-for`)') as Promise<
typeof import('p-wait-for')
>);
let { cid } = await this.add(blob);
cid = cid.toString();

await waitFor(async () => await this.isPinned(cid), {
timeout: { milliseconds: pinTimeout * 1000 },
});
if (!waitForPinned) {
waitForPinned = async (cid) => await this.waitForPinned(cid);
}
await waitForPinned(cid);

return cid.toString();
return cid;
}

async get(cid: string): Promise<string> {
@@ -64,10 +71,22 @@ export class DidStore implements IDidStore {
/**
* Checks if file is pinned on cluster
* @param cid CID
* @param replicationFactor specifies on how many nodes data should be pinned https://ipfscluster.io/documentation/guides/pinning/#replication-factors
*/
async isPinned(cid: string): Promise<boolean> {
return values((await this.status(cid)).peer_map).some(
(pinInfo) => pinInfo.status === TrackerStatus.Pinned
async isPinned(cid: string, replicationFactor = REPLICATION.MIN): Promise<boolean> {
const { replication_factor_min, replication_factor_max } =
await this.allocations(cid);
const expectedReplicationFactor =
replicationFactor === REPLICATION.LOCAL
? 1
: replicationFactor === REPLICATION.MIN
? replication_factor_min
: replication_factor_max;

return (
values((await this.status(cid)).peer_map).filter(
(pinInfo) => pinInfo.status === TrackerStatus.Pinned
).length >= expectedReplicationFactor
);
}

@@ -78,7 +97,7 @@ export class DidStore implements IDidStore {
async status(cid: string): Promise<StatusResponse> {
const path = `pins/${cid}`;

const data = await this.request(path, {
const data = await this.request<StatusResponse>(path, {
method: 'GET',
params: this.params,
});
@@ -95,11 +114,12 @@ export class DidStore implements IDidStore {
const body = new FormData();
body.append('file', file);
try {
const result = await this.request('add', {
const result = await this.request<AddResponse[]>('add', {
method: 'POST',
body,
params: this.params,
});

const data = result[0];
return { ...data, cid: data.cid };
} catch (err) {
@@ -114,7 +134,13 @@ export class DidStore implements IDidStore {
}
}

private async request(
private async waitForPinned(cid: string): Promise<void> {
await waitFor(async () => await this.isPinned(cid), {
timeout: PIN_TIMEOUT * 1000,
});
}

private async request<T>(
path: string,
{
method,
@@ -140,14 +166,19 @@ export class DidStore implements IDidStore {
});

if (response.ok) {
return await response.json();
return (await response.json()) as T;
} else {
throw new Error(
`Can not perform ${method} on endpoint ${endpoint.href}:${response.status}: ${response.statusText}`
);
}
}

private async allocations(cid: string): Promise<PinResponse> {
const path = `allocations/${cid}`;
return this.request(path, { method: 'GET', params: this.params });
}

private encodeParams(
options: Record<string, unknown>
): Record<string, unknown> {
20 changes: 20 additions & 0 deletions packages/did-ipfs-store/src/ipfs.types.ts
Original file line number Diff line number Diff line change
@@ -15,3 +15,23 @@ export type PinInfo = {
export enum TrackerStatus {
Pinned = 'pinned',
}

export type PinResponse = {
replication_factor_min: number;
replication_factor_max: number;
};

export type AddResponse = {
cid: string;
name?: string;
size?: number | string;
bytes?: number | string;
};

export enum REPLICATION {
LOCAL,
MIN,
MAX,
}

export const PIN_TIMEOUT = 3;
37 changes: 37 additions & 0 deletions packages/did-ipfs-store/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const DEFAULT_WAIT_FOR_INTERVAL = 1000;

/**
* @description asynchronously waits for fulfillment of given condition. Throws error if condition wasn't fulfilled in time
*
* @param condition asynchronous funtion which resolves to boolean
* @param opts.timeout time to wait until `condition` is met, in seconds
* @param opts.interval interval between `condition` checking attempts, in seconds
* @returns
*/
export const waitFor = async (
condition: () => Promise<boolean>,
opts: { timeout: number; interval?: number }
) => {
const { timeout, interval = DEFAULT_WAIT_FOR_INTERVAL } = opts;
return new Promise<void>((resolve, reject) => {
let elapsedTime = 0;
const scheduleNextCheck = () =>
setTimeout(async () => {
elapsedTime += interval;
try {
if (await condition()) {
resolve();
} else {
if (elapsedTime > timeout) {
reject();
}
scheduleNextCheck();
}
} catch (_) {
scheduleNextCheck();
}
}, interval);

scheduleNextCheck();
});
};
1 change: 1 addition & 0 deletions packages/did-ipfs-store/test/did-store.test.ts
Original file line number Diff line number Diff line change
@@ -54,6 +54,7 @@ describe('[DID-STORE-PACKAGE]', function () {

after(() => {
shutdownIpfs(cluster);
console.log('cluster has been shutdown');
});
});

Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@
import {
Contract,
ContractFactory,
providers,
ethers,
utils,
constants,

0 comments on commit ccef0e6

Please sign in to comment.