DEV Community 👩‍💻👨‍💻

Cover image for Adding IPFS to my JavaScript OS
Dustin Brett
Dustin Brett

Posted on

Adding IPFS to my JavaScript OS

I've been wanting to dip my toes into the Web 3.0 verse for a while now, specifically with an integration into my side project, a Desktop environment in the browser (daedalOS).

I've decided as a first step I would add ipfs native url support. I've added this support to the Browser (Demo), Run Dialog, Shortcuts & Terminal. Most of the main ipfs functions I've migrated to utils/ipfs.ts.

ipfs://bafkreigh2akiscaildcqabsyg3dfr6chu3fgpregiymsck7e7aqa4s52zy

The basic concept for how this works is by taking the ipfs native url and converting it to one that uses an ipfs gateway, in my case I've added the official ipfs gateways as the fallback and the Cloudflare gateways as the primary.

const IPFS_GATEWAY_URLS = [
  "https://<CID>.ipfs.cf-ipfs.com/",
  "https://<CID>.ipfs.dweb.link/",
  "https://cloudflare-ipfs.com/ipfs/<CID>/",
  "https://gateway.ipfs.io/ipfs/<CID>/",
];
Enter fullscreen mode Exit fullscreen mode

When I make the URL I attempt to use the newer CID v1 format, not to be confused with CID v0 which is less CORS friendly. To convert into this format which uses a case-insensitive base32 format, I used the library multiformats.

let IPFS_GATEWAY_URL = "";

const getIpfsGatewayUrl = async (
  ipfsUrl: string,
  notCurrent?: boolean
): Promise<string> => {
  if (!IPFS_GATEWAY_URL || notCurrent) {
    const urlList = notCurrent
      ? IPFS_GATEWAY_URLS.filter((url) => url !== IPFS_GATEWAY_URL)
      : IPFS_GATEWAY_URLS;

    for (const url of urlList) {
      if (await isIpfsGatewayAvailable(url)) {
        IPFS_GATEWAY_URL = url;
        break;
      }
    }

    if (!IPFS_GATEWAY_URL) return "";
  }

  const { pathname, protocol, search } = new URL(ipfsUrl);

  if (protocol !== "ipfs:") return "";

  const [cid = "", ...path] = pathname.split("/").filter(Boolean);
  const { CID } = await import("multiformats/cid");

  return `${IPFS_GATEWAY_URL.replace(
    "<CID>",
    CID.parse(cid).toV1().toString()
  )}${path.join("/")}${search}`;
};
Enter fullscreen mode Exit fullscreen mode

ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/wiki/Vincent_van_Gogh.html

When doing the initial check to find the gateway to use, I've based the availability checker on code taken from public-gateway-checker.

const isIpfsGatewayAvailable = (gatewayUrl: string): Promise<boolean> =>
  new Promise((resolve) => {
    const timeoutId = window.setTimeout(
      () => resolve(false),
      1000
    );
    const img = new Image();

    img.addEventListener("load", () => {
      window.clearTimeout(timeoutId);
      resolve(true);
    });
    img.addEventListener("error", () => {
      window.clearTimeout(timeoutId);
      resolve(false);
    });

    img.src = `${gatewayUrl.replace(
      "<CID>",
      // https://github.com/ipfs/public-gateway-checker/blob/master/src/constants.ts
      "bafybeibwzifw52ttrkqlikfzext5akxu7lz4xiwjgwzmqcpdzmp3n5vnbe"
    )}?now=${Date.now()}&filename=1x1.png#x-ipfs-companion-no-redirect`;
  });
Enter fullscreen mode Exit fullscreen mode

When it comes to actually doing the request, a normal fetch() command is all that is needed with the new ipfs gateway url.

const getIpfsResource = async (ipfsUrl: string): Promise<Buffer> => {
  let response: Response | null = null;
  const requestOptions = {
    cache: "no-cache",
    credentials: "omit",
    keepalive: false,
    mode: "cors",
    priority: "high",
    referrerPolicy: "no-referrer",
    window: null,
  } as RequestInit;

  try {
    response = await fetch(await getIpfsGatewayUrl(ipfsUrl), requestOptions);
  } catch (error) {
    if ((error as Error).message === "Failed to fetch") {
      response = await fetch(
        await getIpfsGatewayUrl(ipfsUrl, true),
        requestOptions
      );
    }
  }

  return response instanceof Response
    ? Buffer.from(await response.arrayBuffer())
    : Buffer.from("");
};
Enter fullscreen mode Exit fullscreen mode

ipfs://QmcniBv7UQ4gGPQQW2BwbD4ZZHzN3o3tPuNLZCbBchd1zh

I've also added file type detection so it can try to open the url in the correct app. This is done using the library file-type.

const getIpfsFileName = async (
  ipfsUrl: string,
  ipfsData: Buffer
): Promise<string> => {
  const { pathname, searchParams } = new URL(ipfsUrl);
  const fileName = searchParams.get("filename");

  if (fileName) return fileName;

  const { fileTypeFromBuffer } = await import("file-type");
  const { ext = "" } = (await fileTypeFromBuffer(ipfsData)) || {};

  return `${pathname.split("/").filter(Boolean).join("_")}${
    ext ? `.${ext}` : ""
  }`;
};
Enter fullscreen mode Exit fullscreen mode

I've found the reliability of the gateway's to be pretty good, but sometimes there was a large amount of lag before a request would begin.

Thanks for reading my article and feel free to try out some ipfs urls on daedalOS which is also my personal website, dustinbrett.com.

If you want to know more about daedalOS which is the client of all this ipfs magic, feel free to check out my YouTube video where I go over the various features, including ipfs. Thanks!

Top comments (0)

Need a better mental model for async/await?

Check out this classic DEV post on the subject.

⭐️🎀 JavaScript Visualized: Promises & Async/Await

async await