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: add helia/verified-fetch browser example #285

Merged
merged 25 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6b5e262
feat: add helia/verified-fetch browser example
2color Feb 6, 2024
72f3d59
feat(verified-fetch): improve UX (#286)
SgtPooki Feb 8, 2024
9da5dd5
Merge branch 'main' into add-verified-fetch
achingbrain Feb 8, 2024
981bcaa
Update examples/helia-browser-react-verified-fetch/package.json
2color Feb 8, 2024
1ace74a
refactor: clean up code and reduce deps
2color Feb 9, 2024
2ac6a75
chore: rename folder
2color Feb 9, 2024
1d7541c
chore: add test
2color Feb 9, 2024
6959b24
chore: fix linting
achingbrain Feb 9, 2024
32450e4
chore: add test to verified fetch example (#290)
achingbrain Feb 10, 2024
277d818
fix: improve rendering of cids on narrow viewports
2color Feb 12, 2024
17812d5
fix: improve auto content detection
2color Feb 12, 2024
11dafae
chore: fix tests
achingbrain Feb 13, 2024
a6790a4
chore: upgrade verified-fetch
2color Feb 29, 2024
c3f1069
feat: add link to the repo
2color Feb 29, 2024
c715238
chore: bump verified fetch version
2color Mar 14, 2024
26de16f
fix: add aborting functionality
2color Mar 19, 2024
e3924cd
Merge remote-tracking branch 'origin/main' into add-verified-fetch
2color Mar 25, 2024
4098ac3
fix: handle abort error
2color Mar 25, 2024
c4c2228
feat: add ability to fetch as dag-json
2color Mar 27, 2024
6d8c04f
chore: update deps
2color Mar 27, 2024
6da0ce2
chore: remove comment
2color Mar 27, 2024
1cbdf4b
chore: address feedback and simplify code
2color Mar 28, 2024
6148088
chore: reduce code duplication and add docs
2color Mar 28, 2024
d56f4d7
feat: add links to api docs and source
2color Mar 28, 2024
6c0116c
fix: linting error
2color Mar 28, 2024
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
matrix:
project:
- helia-101
- helia-browser-verified-fetch
- helia-cjs
- helia-electron
- helia-esbuild
Expand Down Expand Up @@ -86,6 +87,7 @@ jobs:
project:
- helia-101
- helia-cjs
- helia-browser-verified-fetch
- helia-electron
- helia-esbuild
- helia-jest
Expand Down
24 changes: 24 additions & 0 deletions examples/helia-browser-verified-fetch/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
34 changes: 34 additions & 0 deletions examples/helia-browser-verified-fetch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<p align="center">
<a href="https://github.com/ipfs/helia" title="Helia">
<img src="https://raw.githubusercontent.com/ipfs/helia/main/assets/helia.png" alt="Helia logo" width="300" />
</a>
</p>

<h3 align="center"><b>Browser Verified Retrieval with @helia/verified-fetch</b></h3>

<p align="center">
<img src="https://raw.githubusercontent.com/jlord/forkngo/gh-pages/badges/cobalt.png" width="200">
<br>
<a href="https://helia.io/modules/helia.html">Explore the docs</a>
·
<!-- <a href="https://codesandbox.io/">View Demo</a> -->
·
<a href="https://github.com/ipfs-examples/helia-examples/issues">Report Bug</a>
·
<a href="https://github.com/ipfs-examples/helia-examples/issues">Request Feature/Example</a>
</p>

<!-- omit from toc -->
## Table of Contents

- [Getting Started](#getting-started)
- [Installation and Running example](#installation-and-running-example)

## Getting Started

### Installation and Running example

```console
npm install
npm run dev
```
13 changes: 13 additions & 0 deletions examples/helia-browser-verified-fetch/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com"></script>
<title>Verified Retrieval with @helia/verified-fetch</title>
</head>
<body>
<div class="" id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
29 changes: 29 additions & 0 deletions examples/helia-browser-verified-fetch/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "helia-browser-verified-fetch",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"clean": "rimraf ./dist",
"start": "vite",
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vite build -c ./test/vite.config.js && test-browser-example test"
},
"dependencies": {
"@helia/verified-fetch": "^1.3.2",
"@sgtpooki/file-type": "^1.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@playwright/test": "^1.42.1",
"@types/react": "^18.2.72",
"@types/react-dom": "^18.2.22",
"@vitejs/plugin-react": "^4.2.1",
"test-ipfs-example": "^1.0.0",
"typescript": "^5.4.3",
"vite": "^5.2.6"
}
}
273 changes: 273 additions & 0 deletions examples/helia-browser-verified-fetch/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
import { verifiedFetch } from '@helia/verified-fetch'
import { fileTypeFromBuffer } from '@sgtpooki/file-type'
import { useCallback, useState } from 'react'
import { Output } from './Output'
import { helpText } from './constants'

function App (): JSX.Element {
const [path, setPath] = useState<string>('')
const [output, setOutput] = useState<string | JSX.Element>('')
const [err, setErr] = useState<string>('')
const [loading, setLoadingTo] = useState<string>('')
const [controller, setController] = useState<AbortController | null>(null)

const setSuccess = useCallback((message: string | JSX.Element) => {
setOutput(message)
setLoadingTo('')
setErr('')
}, [])
const setError = useCallback((message: string) => {
setOutput('')
setLoadingTo('')
setErr(message)
}, [])
const setLoading = useCallback((message: string) => {
setErr('')
setLoadingTo(message)
}, [])

const handleImageType = useCallback(async (resp: Response) => {
try {
setLoading('Waiting for full image data...')
const blob = await resp.blob()
const url = URL.createObjectURL(blob)
setSuccess(<img src={url} alt="fetched image content" />)
} catch (err) {
setError((err as Error).message)
}
}, [])

const handleJsonType = useCallback(async (resp: Response) => {
try {
setLoading('Waiting for full JSON data...')
const json = await resp.json()
setSuccess(JSON.stringify(json, null, 2))
} catch (err) {
setError((err as Error).message)
}
}, [])

const handleVideoType = useCallback(async (resp: Response) => {
try {
controller?.abort() // abort any ongoing requests
setLoading('Waiting for full video data...')
const blob = await resp.blob()
const url = URL.createObjectURL(blob)
setSuccess(<video controls src={url} />)
} catch (err) {
setError((err as Error).message)
}
}, [])

const onFetchJson = useCallback(async (jsonType: 'json' | 'dag-json' = 'json') => {
try {
controller?.abort() // abort any ongoing requests
setLoading(`Fetching ${jsonType} response...`)
const ctl = new AbortController()
setController(ctl)
const resp = await verifiedFetch(path, {
signal: ctl.signal,
headers: {
accept: jsonType === 'json' ? 'application/json' : 'application/vnd.ipld.dag-json'
}
})
await handleJsonType(resp)
} catch (err: any) {
// TODO: simplify AbortErr handling to use err.name once https://github.com/libp2p/js-libp2p/pull/2446 is merged
if (err?.code === 'ABORT_ERR') {
return
}
if (err instanceof Error) {
setError(err.message)
}
}
}, [path, handleJsonType])

const onFetchImage = useCallback(async () => {
try {
controller?.abort() // abort any ongoing requests
setLoading('Fetching image response...')
const ctl = new AbortController()
setController(ctl)
const resp = await verifiedFetch(path, { signal: ctl.signal })
await handleImageType(resp)
} catch (err: any) {
if (err?.code === 'ABORT_ERR') {
return
}
// Don't render AbortErrors since they are user intiated
if (err instanceof Error) {
setError(err.message)
}
}
}, [path, handleImageType])

const onFetchFile = useCallback(async () => {
try {
controller?.abort() // abort any ongoing requests
setLoading('Fetching content to download...')
const ctl = new AbortController()
setController(ctl)
const resp = await verifiedFetch(path, { signal: ctl.signal })
const blob = await resp.blob()
const url = URL.createObjectURL(blob)
const downloadLink = document.createElement('a')
downloadLink.href = url
downloadLink.download = 'download'
setSuccess('') // clear output
downloadLink.click()
} catch (err: any) {
if (err?.code === 'ABORT_ERR') {
return
}
// Don't render AbortErrors since they are user intiated
if (err instanceof Error) {
setError(err.message)
}
}
}, [path])

const onAbort = useCallback(async () => {
if (controller != null) {
controller.abort('Rqeuest aborted')
setLoadingTo('')
}
}, [controller])

const onPathChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
setPath(e.target.value)
}, [])

const onFetchAuto = useCallback(async () => {
if (path == null) {
setError('Invalid path')
return
}
try {
controller?.abort() // abort any ongoing requests
setLoading('Fetching with automatic content detection...')
const ctl = new AbortController()
setController(ctl)
const resp = await verifiedFetch(path, { signal: ctl.signal })
const buffer = await resp.clone().arrayBuffer()
let contentType = (await fileTypeFromBuffer(new Uint8Array(buffer)))?.mime
if (!contentType) {
try {
// see if we can parse as json
await resp.clone().json()
contentType = 'application/json'
} catch (err) {
// ignore
}
}
switch (true) {
case contentType?.includes('image'):
await handleImageType(resp)
break
case contentType?.includes('json'):
await handleJsonType(resp)
break
case contentType?.includes('video'):
await handleVideoType(resp)
break
default:
setError(`Unknown content-type: ${contentType}`)
}
} catch (err: any) {
if (err?.code === 'ABORT_ERR') {
return
}
// Don't render AbortErrors since they are user intiated
if (err instanceof Error) {
setError(err.message)
}
}
}, [path, handleImageType, handleJsonType, handleVideoType])

return (
<div className="">
<section>
<div className="grid h-screen grid-cols-2">
{/* Left 👇 */}
<div className="bg-teal-200 p-4">
<div className="flex items-center space-x-4">
<a className="" href="https://github.com/ipfs/helia">
<img
className="h-20"
alt="Helia logo"
src="https://unpkg.com/@helia/css@1.0.1/logos/helia-logo.svg"
/>
</a>
<h1 className="text-2xl">
Verified Retrieval with <a href="https://github.com/ipfs/helia-verified-fetch/tree/main/packages/verified-fetch"><strong className='underline'>@helia/verified-fetch</strong></a>
</h1>
</div>
<label className="block mt-4 mb-2 font-medium text-gray-900">
IPFS path to fetch
</label>
<input
type="text"
id="ipfs-path"
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="ipfs://... or ipns://"
onChange={onPathChange}
value={path}
/>
<button
className="my-2 mr-2 btn btn-blue bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
id="button-fetch-json"
onClick={async () => onFetchJson('json')}
>
🔑 Fetch as JSON
</button>
<button
className="my-2 mr-2 btn btn-blue bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
id="button-fetch-dag-json"
onClick={async () => onFetchJson('dag-json')}
>
🔑 Fetch as dag-json
</button>
<button
className="my-2 mr-2 btn btn-blue bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
id="button-fetch-image"
onClick={onFetchImage}
>
🔑 Fetch as image
</button>
<button
className="my-2 mr-2 btn btn-blue bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
id="button-fetch-file"
onClick={onFetchFile}
>
🔑 Fetch & Download
</button>
<button
className="my-2 mr-2 btn btn-blue bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
id="button-fetch-auto"
onClick={onFetchAuto}
>
🔑 Fetch auto
</button>
<button
className="my-2 mr-2 btn btn-blue bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
id="button-fetch-auto"
onClick={onAbort}
>
❌ Abort request
</button>

<pre className="bg-black text-teal-300 rounded p-4 whitespace-pre-wrap break-words">{helpText}</pre>
<a href="https://github.com/ipfs-examples/helia-examples/tree/main/examples/helia-browser-verified-fetch" className="text-2xl block mt-2 underline">Source for example</a>
<a href="https://github.com/ipfs/helia-verified-fetch/tree/main/packages/verified-fetch" className="text-2xl block mt-2 underline"><code>@helia/verified-fetch</code> API Docs</a>

</div>

{/* Right 👇 */}
<Output loading={loading} output={output} err={err} />
</div>
</section>
</div>
)
}

export default App
Loading
Loading