secureweb/ipfs-implementation-plan.md
Mahmoud Emad 3e1822247d feat: Implement IPFS functionality using Helia
- Add Helia and related dependencies for IPFS integration.
- Create IPFS service module for core IPFS operations.
- Create IPFS context provider for application-wide access.
- Modify MarkdownContent component to fetch from IPFS.
- Create IPFS uploader component for content upload.
- Create IPFS gateway fallback for offline access.
- Modify NavDataProvider to load from IPFS.
- Implement offline support and local caching.
- Create Network Status Service to monitor network status.
- Create Offline Status Component to display offline status.
- Implement Service Worker for caching app assets.
- Create Offline page.
2025-05-13 09:31:14 +03:00

1446 lines
37 KiB
Markdown

# IPFS Implementation Plan for SecureWeb Project
## Overview
This document outlines the plan for implementing IPFS functionality in the SecureWeb project using Helia, a modern TypeScript implementation of IPFS designed for JavaScript environments.
## Current State Analysis
### Project Structure
- Svelte-based application with TypeScript
- Content currently loaded from local files using fetch
- Markdown rendering using the marked library
- Navigation data loaded from a JSON file
### Requirements
- Implement both content upload and retrieval functionality using Helia
- Ensure compatibility across all modern browsers
- Leverage Helia's TypeScript support and modern architecture
## Implementation Plan
### 1. Add Helia Dependencies
```bash
# Core Helia packages
npm install @helia/interface @helia/unixfs
# For browser environment
npm install @libp2p/webrtc @libp2p/websockets @libp2p/webtransport
# For content handling
npm install multiformats
```
### 2. Create IPFS Service Module
Create a service module to handle IPFS operations:
```typescript
// src/services/ipfs.service.ts
import { createHelia } from '@helia/interface'
import { unixfs } from '@helia/unixfs'
import type { Helia } from '@helia/interface'
import type { UnixFS } from '@helia/unixfs'
import { CID } from 'multiformats/cid'
class IPFSService {
private helia: Helia | null = null
private fs: UnixFS | null = null
async initialize() {
try {
// Create a Helia instance
this.helia = await createHelia({
// Configuration options
})
// Create a UnixFS instance for file operations
this.fs = unixfs(this.helia)
console.log('IPFS initialized successfully')
return true
} catch (error) {
console.error('Failed to initialize IPFS:', error)
return false
}
}
async getContent(cidStr: string): Promise<string> {
if (!this.fs) {
throw new Error('IPFS not initialized')
}
try {
const cid = CID.parse(cidStr)
// Fetch content from IPFS
const decoder = new TextDecoder()
let content = ''
for await (const chunk of this.fs.cat(cid)) {
content += decoder.decode(chunk, { stream: true })
}
return content
} catch (error) {
console.error(`Failed to get content for CID ${cidStr}:`, error)
throw error
}
}
async getImage(cidStr: string): Promise<Blob> {
if (!this.fs) {
throw new Error('IPFS not initialized')
}
try {
const cid = CID.parse(cidStr)
// Fetch image data from IPFS
const chunks = []
for await (const chunk of this.fs.cat(cid)) {
chunks.push(chunk)
}
// Combine chunks into a single Uint8Array
const allChunks = new Uint8Array(
chunks.reduce((acc, chunk) => acc + chunk.length, 0)
)
let offset = 0
for (const chunk of chunks) {
allChunks.set(chunk, offset)
offset += chunk.length
}
// Create a Blob from the Uint8Array
return new Blob([allChunks])
} catch (error) {
console.error(`Failed to get image for CID ${cidStr}:`, error)
throw error
}
}
async uploadContent(content: string): Promise<string> {
if (!this.fs) {
throw new Error('IPFS not initialized')
}
try {
const encoder = new TextEncoder()
const cid = await this.fs.addBytes(encoder.encode(content))
return cid.toString()
} catch (error) {
console.error('Failed to upload content:', error)
throw error
}
}
async uploadFile(file: File): Promise<string> {
if (!this.fs) {
throw new Error('IPFS not initialized')
}
try {
const buffer = await file.arrayBuffer()
const cid = await this.fs.addBytes(new Uint8Array(buffer))
return cid.toString()
} catch (error) {
console.error('Failed to upload file:', error)
throw error
}
}
}
// Create a singleton instance
export const ipfsService = new IPFSService()
```
### 3. Create IPFS Context Provider
Create a Svelte context to provide IPFS functionality throughout the application:
```typescript
// src/lib/contexts/ipfs-context.ts
import { createContext } from 'svelte'
import { ipfsService } from '../../services/ipfs.service'
export const IPFSContext = createContext('ipfs')
export function createIPFSContext() {
const initialize = async () => {
return await ipfsService.initialize()
}
const getContent = async (cid: string) => {
return await ipfsService.getContent(cid)
}
const getImage = async (cid: string) => {
return await ipfsService.getImage(cid)
}
const uploadContent = async (content: string) => {
return await ipfsService.uploadContent(content)
}
const uploadFile = async (file: File) => {
return await ipfsService.uploadFile(file)
}
return {
initialize,
getContent,
getImage,
uploadContent,
uploadFile
}
}
```
### 4. Create IPFS Provider Component
Create a component to provide IPFS context to the application:
```svelte
<!-- src/components/IPFSProvider.svelte -->
<script lang="ts">
import { onMount } from 'svelte'
import { IPFSContext, createIPFSContext } from '../lib/contexts/ipfs-context'
const ipfs = createIPFSContext()
let initialized = false
let error = null
onMount(async () => {
try {
initialized = await ipfs.initialize()
} catch (err) {
error = err
console.error('Failed to initialize IPFS:', err)
}
})
</script>
<IPFSContext.Provider value={ipfs}>
{#if error}
<div class="error">
Failed to initialize IPFS: {error.message}
</div>
{:else if !initialized}
<div class="loading">
Initializing IPFS...
</div>
{:else}
<slot />
{/if}
</IPFSContext.Provider>
```
### 5. Modify App Component to Include IPFS Provider
Update the main App component to include the IPFS provider:
```svelte
<!-- src/App.svelte (modified) -->
<script lang="ts">
import IPFSProvider from './components/IPFSProvider.svelte'
import Layout from './components/Layout.svelte'
import Home from './components/Home.svelte'
import './app.css'
</script>
<IPFSProvider>
<Layout>
<Home />
</Layout>
</IPFSProvider>
```
### 6. Create IPFS Metadata Service
Create a service to handle metadata retrieval and storage:
```typescript
// src/services/ipfs-metadata.service.ts
import { ipfsService } from './ipfs.service'
import type { NavItem } from '../types/nav'
// Extended NavItem interface with IPFS CIDs
export interface IPFSNavItem extends NavItem {
contentCid?: string
children?: IPFSNavItem[]
}
class IPFSMetadataService {
private metadataCache: Map<string, any> = new Map()
async getMetadata(cid: string): Promise<any> {
// Check cache first
if (this.metadataCache.has(cid)) {
return this.metadataCache.get(cid)
}
try {
const content = await ipfsService.getContent(cid)
const metadata = JSON.parse(content)
// Cache the result
this.metadataCache.set(cid, metadata)
return metadata
} catch (error) {
console.error(`Failed to get metadata for CID ${cid}:`, error)
throw error
}
}
async uploadMetadata(metadata: any): Promise<string> {
try {
const content = JSON.stringify(metadata, null, 2)
return await ipfsService.uploadContent(content)
} catch (error) {
console.error('Failed to upload metadata:', error)
throw error
}
}
async uploadNavData(navData: IPFSNavItem[]): Promise<string> {
return await this.uploadMetadata(navData)
}
}
export const ipfsMetadataService = new IPFSMetadataService()
```
### 7. Modify MarkdownContent Component to Use IPFS
Update the MarkdownContent component to fetch content from IPFS:
```svelte
<!-- src/components/MarkdownContent.svelte (modified) -->
<script lang="ts">
import { onMount } from 'svelte'
import { marked } from 'marked'
import { IPFSContext } from '../lib/contexts/ipfs-context'
export let path: string = ""
export let contentCid: string = ""
let content = ""
let loading = true
let error: string | null = null
const ipfs = IPFSContext.consume()
$: if (path || contentCid) {
loadContent()
}
async function loadContent() {
loading = true
error = null
content = ""
try {
let markdown = ""
if (contentCid) {
// Load from IPFS
markdown = await ipfs.getContent(contentCid)
} else if (path) {
// Fallback to traditional loading
// Remove leading slash if present
const cleanPath = path.startsWith("/")
? path.substring(1)
: path
// If path is just a section like "introduction", append "/introduction" to it
const finalPath = cleanPath.includes("/")
? cleanPath
: `${cleanPath}/${cleanPath}`
console.log(`Loading markdown from: /src/docs/${finalPath}.md`)
const response = await fetch(`/src/docs/${finalPath}.md`)
if (!response.ok) {
throw new Error(
`Failed to load content: ${response.status} ${response.statusText}`
)
}
markdown = await response.text()
// Process markdown to fix image paths for traditional loading
// Replace relative image paths with absolute paths
const docDir = finalPath.substring(0, finalPath.lastIndexOf("/"))
markdown = markdown.replace(
/!\[(.*?)\]\((?!http|\/)(.*?)\)/g,
(_match, alt, imgPath) => {
return `![${alt}](/images/${docDir}/${imgPath})`
}
)
} else {
throw new Error("No path or CID provided")
}
// If using IPFS, process image references
if (contentCid) {
// Replace IPFS image references with blob URLs
// This is a simplified example - actual implementation would depend on how images are referenced
markdown = await processIPFSImageReferences(markdown)
}
const parsedContent = marked.parse(markdown)
content = typeof parsedContent === "string"
? parsedContent
: await parsedContent
} catch (err: any) {
console.error("Error loading content:", err)
error = err.message || "Failed to load content"
} finally {
loading = false
}
}
async function processIPFSImageReferences(markdown: string): Promise<string> {
// This is a simplified example - actual implementation would depend on how images are referenced
// For example, if images are referenced as ipfs://Qm...
const ipfsImageRegex = /!\[(.*?)\]\(ipfs:\/\/(.*?)\)/g
const promises: Promise<string>[] = []
let match
const replacements: [string, string][] = []
while ((match = ipfsImageRegex.exec(markdown)) !== null) {
const [fullMatch, alt, cid] = match
promises.push(
(async () => {
try {
const imageBlob = await ipfs.getImage(cid)
const imageUrl = URL.createObjectURL(imageBlob)
replacements.push([fullMatch, `![${alt}](${imageUrl})`])
return "success"
} catch (error) {
console.error(`Failed to load image with CID ${cid}:`, error)
return "error"
}
})()
)
}
await Promise.all(promises)
let processedMarkdown = markdown
for (const [original, replacement] of replacements) {
processedMarkdown = processedMarkdown.replace(original, replacement)
}
return processedMarkdown
}
onMount(() => {
if (path || contentCid) {
loadContent()
}
})
</script>
<div class="markdown-content">
{#if loading}
<div class="loading">
<p>Loading content...</p>
</div>
{:else if error}
<div class="error">
<p>Error: {error}</p>
</div>
{:else}
<div class="content">
{@html content}
</div>
{/if}
</div>
### 8. Create Content Upload Component
Create a new component for uploading content to IPFS:
```svelte
<!-- src/components/IPFSUploader.svelte -->
<script lang="ts">
import { IPFSContext } from '../lib/contexts/ipfs-context'
export let onUploadComplete: (cid: string) => void = () => {}
let content = ""
let file: File | null = null
let uploading = false
let error: string | null = null
let uploadedCid: string | null = null
const ipfs = IPFSContext.consume()
async function uploadContent() {
if (!content.trim()) {
error = "Content cannot be empty"
return
}
uploading = true
error = null
uploadedCid = null
try {
const cid = await ipfs.uploadContent(content)
uploadedCid = cid
onUploadComplete(cid)
} catch (err: any) {
error = err.message || "Failed to upload content"
} finally {
uploading = false
}
}
async function uploadFile() {
if (!file) {
error = "No file selected"
return
}
uploading = true
error = null
uploadedCid = null
try {
const cid = await ipfs.uploadFile(file)
uploadedCid = cid
onUploadComplete(cid)
} catch (err: any) {
error = err.message || "Failed to upload file"
} finally {
uploading = false
}
}
function handleFileChange(event: Event) {
const input = event.target as HTMLInputElement
if (input.files && input.files.length > 0) {
file = input.files[0]
}
}
</script>
<div class="ipfs-uploader">
<h2>Upload to IPFS</h2>
<div class="tabs">
<button class="tab" class:active={!file} on:click={() => file = null}>
Text Content
</button>
<button class="tab" class:active={!!file} on:click={() => content = ""}>
File Upload
</button>
</div>
{#if !file}
<!-- Text content upload -->
<div class="content-upload">
<textarea
bind:value={content}
placeholder="Enter content to upload to IPFS..."
rows="10"
disabled={uploading}
></textarea>
<button
on:click={uploadContent}
disabled={uploading || !content.trim()}
class="upload-button"
>
{uploading ? 'Uploading...' : 'Upload Content'}
</button>
</div>
{:else}
<!-- File upload -->
<div class="file-upload">
<input
type="file"
on:change={handleFileChange}
disabled={uploading}
/>
<button
on:click={uploadFile}
disabled={uploading || !file}
class="upload-button"
>
{uploading ? 'Uploading...' : 'Upload File'}
</button>
</div>
{/if}
{#if error}
<div class="error">
<p>{error}</p>
</div>
{/if}
{#if uploadedCid}
<div class="success">
<p>Upload successful!</p>
<p>CID: <code>{uploadedCid}</code></p>
</div>
{/if}
</div>
```
### 9. Create IPFS Gateway Fallback Service
Implement a fallback mechanism to use public IPFS gateways when direct IPFS connection fails:
```typescript
// src/services/ipfs-gateway.service.ts
class IPFSGatewayService {
private gateways = [
'https://ipfs.io/ipfs/',
'https://gateway.pinata.cloud/ipfs/',
'https://cloudflare-ipfs.com/ipfs/',
'https://dweb.link/ipfs/'
]
async fetchFromGateway(cid: string): Promise<Response> {
// Try gateways in order until one succeeds
for (const gateway of this.gateways) {
try {
const response = await fetch(`${gateway}${cid}`)
if (response.ok) {
return response
}
} catch (error) {
console.warn(`Gateway ${gateway} failed for CID ${cid}:`, error)
}
}
throw new Error(`All gateways failed for CID ${cid}`)
}
async getContent(cid: string): Promise<string> {
const response = await this.fetchFromGateway(cid)
return await response.text()
}
async getImage(cid: string): Promise<Blob> {
const response = await this.fetchFromGateway(cid)
return await response.blob()
}
}
export const ipfsGatewayService = new IPFSGatewayService()
```
### 10. Modify NavDataProvider to Support IPFS
Update the NavDataProvider component to support loading navigation data from IPFS:
```svelte
<!-- src/components/NavDataProvider.svelte (modified) -->
<script lang="ts">
import type { NavItem } from '../types/nav'
import { onMount } from 'svelte'
import { IPFSContext } from '../lib/contexts/ipfs-context'
import type { IPFSNavItem } from '../services/ipfs-metadata.service'
export let navDataCid: string = ""
let navData: NavItem[] = []
let loading = true
let error: string | null = null
const ipfs = IPFSContext.consume()
onMount(async () => {
try {
if (navDataCid) {
// Load from IPFS
const content = await ipfs.getContent(navDataCid)
navData = JSON.parse(content)
} else {
// Fallback to traditional loading
const response = await fetch('/src/data/navData.json')
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
navData = await response.json()
}
} catch (e: any) {
error = e.message
} finally {
loading = false
}
})
</script>
{#if loading}
<p>Loading navigation data...</p>
{:else if error}
<p>Error loading navigation data: {error}</p>
{:else}
<slot {navData}></slot>
{/if}
```
## Implementation Workflow
Here's a step-by-step workflow for implementing the IPFS functionality:
1. **Add Dependencies**: Install Helia and related packages
2. **Create Core Services**: Implement IPFS service, metadata service, and gateway fallback
3. **Create Context Provider**: Set up IPFS context for the application
4. **Update Components**: Modify existing components to use IPFS for content retrieval
5. **Add Upload Components**: Create components for content upload
6. **Testing**: Test the implementation with various content types and network conditions
7. **Documentation**: Document the IPFS implementation for future reference
## Architecture Diagram
```mermaid
graph TD
A[App Component] --> B[IPFS Provider]
B --> C[Layout Component]
C --> D[MarkdownContent Component]
D --> E[IPFS Service]
E --> F[Helia Client]
F --> G[IPFS Network]
E --> H[IPFS Gateway Service]
H --> I[Public IPFS Gateways]
I --> G
J[IPFSUploader Component] --> E
L[NavDataProvider] --> E
M[IPFS Metadata Service] --> E
```
## Security Considerations
1. **Content Integrity**: Leverage IPFS content addressing to ensure content integrity
2. **Metadata Integrity**: Implement verification of metadata CIDs
3. **IPFS Client Security**: Use the well-vetted Helia library
4. **Content Rendering Security**: Sanitize markdown content before rendering
5. **Error Handling**: Implement robust error handling to prevent security issues
6. **Fallback Mechanisms**: Ensure fallback mechanisms don't compromise security
## Testing Strategy
1. **Unit Testing**: Test individual components and services
- Test IPFS service methods with mock data
- Test context provider with mock IPFS service
- Test components with mock context
2. **Integration Testing**: Test the interaction between components
- Test content retrieval flow from IPFS to rendered content
- Test content upload flow from form to IPFS
3. **Browser Compatibility**: Test across different browsers
- Chrome, Firefox, Safari, Edge
- Mobile browsers
4. **Network Conditions**: Test under various network conditions
- Fast connection
- Slow connection
- Intermittent connection
## Conclusion
This implementation plan provides a comprehensive approach to integrating IPFS functionality into the SecureWeb project using Helia. By following this plan, the project will be able to leverage the benefits of IPFS for content storage and retrieval while maintaining compatibility with modern browsers and providing a good user experience.
## Offline Functionality and Local Caching
To ensure the application works effectively in offline scenarios and provides a smooth user experience even with intermittent connectivity, we'll implement a comprehensive offline functionality and local caching strategy.
### 11. Implement Offline Support and Caching
#### 11.1 Create IPFS Cache Service
Create a service to handle local caching of IPFS content using IndexedDB:
```typescript
// src/services/ipfs-cache.service.ts
import { openDB, DBSchema, IDBPDatabase } from 'idb'
import { CID } from 'multiformats/cid'
interface IPFSCacheDB extends DBSchema {
'ipfs-content': {
key: string;
value: {
cid: string;
content: string;
timestamp: number;
contentType: string;
};
indexes: { 'by-timestamp': number };
};
}
class IPFSCacheService {
private db: IDBPDatabase<IPFSCacheDB> | null = null
private readonly DB_NAME = 'ipfs-cache'
private readonly STORE_NAME = 'ipfs-content'
private readonly MAX_CACHE_SIZE = 50 * 1024 * 1024 // 50MB
private readonly MAX_CACHE_AGE = 7 * 24 * 60 * 60 * 1000 // 7 days
async initialize(): Promise<boolean> {
try {
this.db = await openDB<IPFSCacheDB>(this.DB_NAME, 1, {
upgrade(db) {
const store = db.createObjectStore('ipfs-content', { keyPath: 'cid' })
store.createIndex('by-timestamp', 'timestamp')
}
})
// Clean up old cache entries on initialization
await this.cleanupCache()
return true
} catch (error) {
console.error('Failed to initialize IPFS cache:', error)
return false
}
}
async cacheContent(cid: string, content: string, contentType: string = 'text/plain'): Promise<void> {
if (!this.db) {
throw new Error('Cache not initialized')
}
try {
await this.db.put(this.STORE_NAME, {
cid,
content,
timestamp: Date.now(),
contentType
})
// Check cache size and clean up if necessary
await this.checkCacheSize()
} catch (error) {
console.error(`Failed to cache content for CID ${cid}:`, error)
throw error
}
}
async cacheBlob(cid: string, blob: Blob): Promise<void> {
if (!this.db) {
throw new Error('Cache not initialized')
}
try {
// Convert blob to base64 string for storage
const reader = new FileReader()
const contentPromise = new Promise<string>((resolve, reject) => {
reader.onload = () => resolve(reader.result as string)
reader.onerror = reject
})
reader.readAsDataURL(blob)
const content = await contentPromise
await this.db.put(this.STORE_NAME, {
cid,
content,
timestamp: Date.now(),
contentType: blob.type || 'application/octet-stream'
})
// Check cache size and clean up if necessary
await this.checkCacheSize()
} catch (error) {
console.error(`Failed to cache blob for CID ${cid}:`, error)
throw error
}
}
async getContent(cid: string): Promise<string | null> {
if (!this.db) {
throw new Error('Cache not initialized')
}
try {
const entry = await this.db.get(this.STORE_NAME, cid)
if (!entry || entry.contentType !== 'text/plain') {
return null
}
// Update timestamp to mark as recently used
await this.db.put(this.STORE_NAME, {
...entry,
timestamp: Date.now()
})
return entry.content
} catch (error) {
console.error(`Failed to get cached content for CID ${cid}:`, error)
return null
}
}
async getBlob(cid: string): Promise<Blob | null> {
if (!this.db) {
throw new Error('Cache not initialized')
}
try {
const entry = await this.db.get(this.STORE_NAME, cid)
if (!entry || entry.contentType === 'text/plain') {
return null
}
// Update timestamp to mark as recently used
await this.db.put(this.STORE_NAME, {
...entry,
timestamp: Date.now()
})
// Convert base64 string back to blob
const response = await fetch(entry.content)
return await response.blob()
} catch (error) {
console.error(`Failed to get cached blob for CID ${cid}:`, error)
return null
}
}
async hasCached(cid: string): Promise<boolean> {
if (!this.db) {
throw new Error('Cache not initialized')
}
try {
const entry = await this.db.get(this.STORE_NAME, cid)
return !!entry
} catch (error) {
console.error(`Failed to check cache for CID ${cid}:`, error)
return false
}
}
async removeFromCache(cid: string): Promise<void> {
if (!this.db) {
throw new Error('Cache not initialized')
}
try {
await this.db.delete(this.STORE_NAME, cid)
} catch (error) {
console.error(`Failed to remove CID ${cid} from cache:`, error)
throw error
}
}
async clearCache(): Promise<void> {
if (!this.db) {
throw new Error('Cache not initialized')
}
try {
await this.db.clear(this.STORE_NAME)
} catch (error) {
console.error('Failed to clear cache:', error)
throw error
}
}
private async cleanupCache(): Promise<void> {
if (!this.db) {
return
}
try {
const now = Date.now()
const expiredTimestamp = now - this.MAX_CACHE_AGE
// Get all entries older than MAX_CACHE_AGE
const tx = this.db.transaction(this.STORE_NAME, 'readwrite')
const index = tx.store.index('by-timestamp')
let cursor = await index.openCursor(IDBKeyRange.upperBound(expiredTimestamp))
// Delete expired entries
while (cursor) {
await cursor.delete()
cursor = await cursor.continue()
}
await tx.done
} catch (error) {
console.error('Failed to clean up cache:', error)
}
}
private async checkCacheSize(): Promise<void> {
if (!this.db) {
return
}
try {
// Get all entries
const entries = await this.db.getAll(this.STORE_NAME)
// Calculate total size
let totalSize = 0
for (const entry of entries) {
totalSize += entry.content.length
}
// If cache is too large, remove oldest entries
if (totalSize > this.MAX_CACHE_SIZE) {
// Sort by timestamp (oldest first)
entries.sort((a, b) => a.timestamp - b.timestamp)
// Remove oldest entries until we're under the limit
const tx = this.db.transaction(this.STORE_NAME, 'readwrite')
for (const entry of entries) {
await tx.store.delete(entry.cid)
totalSize -= entry.content.length
if (totalSize <= this.MAX_CACHE_SIZE * 0.8) { // Reduce to 80% of max
break
}
}
await tx.done
}
} catch (error) {
console.error('Failed to check cache size:', error)
}
}
}
export const ipfsCacheService = new IPFSCacheService()
```
#### 11.2 Modify IPFS Service to Use Cache
Update the IPFS service to integrate with the cache service:
```typescript
// src/services/ipfs.service.ts (modified)
import { ipfsCacheService } from './ipfs-cache.service'
import { ipfsGatewayService } from './ipfs-gateway.service'
import { networkService } from './network.service'
// Inside IPFSService class
async initialize() {
try {
// Initialize cache first
await ipfsCacheService.initialize()
// Create a Helia instance
this.helia = await createHelia({
// Configuration options
})
// Create a UnixFS instance for file operations
this.fs = unixfs(this.helia)
console.log('IPFS initialized successfully')
return true
} catch (error) {
console.error('Failed to initialize IPFS:', error)
return false
}
}
async getContent(cidStr: string): Promise<string> {
// Check cache first
try {
const cachedContent = await ipfsCacheService.getContent(cidStr)
if (cachedContent) {
console.log(`Retrieved content for CID ${cidStr} from cache`)
return cachedContent
}
} catch (error) {
console.warn(`Cache retrieval failed for CID ${cidStr}:`, error)
}
// If offline and not in cache, throw error
if (!networkService.isOnline()) {
throw new Error('Cannot retrieve content: You are offline and the content is not available in the cache')
}
// Not in cache, try to get from IPFS
if (!this.fs) {
throw new Error('IPFS not initialized')
}
try {
const cid = CID.parse(cidStr)
// Fetch content from IPFS
const decoder = new TextDecoder()
let content = ''
for await (const chunk of this.fs.cat(cid)) {
content += decoder.decode(chunk, { stream: true })
}
// Cache the content
try {
await ipfsCacheService.cacheContent(cidStr, content, 'text/plain')
} catch (cacheError) {
console.warn(`Failed to cache content for CID ${cidStr}:`, cacheError)
}
return content
} catch (error) {
console.warn(`Direct IPFS retrieval failed for CID ${cidStr}, trying gateways:`, error)
try {
// Try gateway fallback
const content = await ipfsGatewayService.getContent(cidStr)
// Cache the content
try {
await ipfsCacheService.cacheContent(cidStr, content, 'text/plain')
} catch (cacheError) {
console.warn(`Failed to cache content from gateway for CID ${cidStr}:`, cacheError)
}
return content
} catch (gatewayError) {
console.error(`Gateway fallback also failed for CID ${cidStr}:`, gatewayError)
throw new Error(`Failed to get content: ${gatewayError.message}`)
}
}
}
// Similar modifications for getImage method
```
#### 11.3 Create Network Status Service
Create a service to monitor network status:
```typescript
// src/services/network.service.ts
class NetworkService {
private online: boolean = navigator.onLine
private listeners: Set<(online: boolean) => void> = new Set()
constructor() {
// Initialize with current online status
this.online = navigator.onLine
// Add event listeners for online/offline events
window.addEventListener('online', this.handleOnline.bind(this))
window.addEventListener('offline', this.handleOffline.bind(this))
}
isOnline(): boolean {
return this.online
}
addStatusChangeListener(listener: (online: boolean) => void): () => void {
this.listeners.add(listener)
// Return function to remove listener
return () => {
this.listeners.delete(listener)
}
}
private handleOnline() {
this.online = true
this.notifyListeners()
}
private handleOffline() {
this.online = false
this.notifyListeners()
}
private notifyListeners() {
for (const listener of this.listeners) {
listener(this.online)
}
}
}
export const networkService = new NetworkService()
```
#### 11.4 Create Offline Status Component
Create a component to display offline status:
```svelte
<!-- src/components/OfflineStatus.svelte -->
<script lang="ts">
import { onMount, onDestroy } from 'svelte'
import { networkService } from '../services/network.service'
import { Wifi, WifiOff } from 'lucide-svelte'
let online = networkService.isOnline()
let removeListener: (() => void) | null = null
onMount(() => {
removeListener = networkService.addStatusChangeListener((status) => {
online = status
})
})
onDestroy(() => {
if (removeListener) {
removeListener()
}
})
</script>
{#if !online}
<div class="offline-banner">
<WifiOff class="icon" />
<span>You are offline. Some content may not be available.</span>
</div>
{/if}
<style>
.offline-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #f59e0b;
color: white;
padding: 0.5rem 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
z-index: 50;
animation: slide-up 0.3s ease-out;
}
.icon {
width: 1.25rem;
height: 1.25rem;
}
@keyframes slide-up {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
</style>
```
#### 11.5 Modify App Component to Include Offline Status
Update the App component to include the offline status component:
```svelte
<!-- src/App.svelte (modified) -->
<script lang="ts">
import IPFSProvider from './components/IPFSProvider.svelte'
import Layout from './components/Layout.svelte'
import Home from './components/Home.svelte'
import OfflineStatus from './components/OfflineStatus.svelte'
import './app.css'
</script>
<IPFSProvider>
<Layout>
<Home />
</Layout>
<OfflineStatus />
</IPFSProvider>
```
#### 11.6 Implement Service Worker for Offline Access
Create a service worker to cache application assets:
```typescript
// src/service-worker.ts
/// <reference lib="webworker" />
const CACHE_NAME = 'secureweb-cache-v1'
const ASSETS_TO_CACHE = [
'/',
'/index.html',
'/app.css',
'/main.js',
// Add other essential assets
]
self.addEventListener('install', (event: ExtendableEvent) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(ASSETS_TO_CACHE)
})
)
})
self.addEventListener('activate', (event: ExtendableEvent) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
)
})
)
})
self.addEventListener('fetch', (event: FetchEvent) => {
// Skip IPFS requests - these are handled by our IPFS cache
if (event.request.url.includes('/ipfs/')) {
return
}
event.respondWith(
caches.match(event.request).then((response) => {
// Return cached response if available
if (response) {
return response
}
// Clone the request because it's a one-time use stream
const fetchRequest = event.request.clone()
return fetch(fetchRequest).then((response) => {
// Check if valid response
if (!response || response.status !== 200 || response.type !== 'basic') {
return response
}
// Clone the response because it's a one-time use stream
const responseToCache = response.clone()
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache)
})
return response
}).catch(() => {
// If fetch fails (offline), try to return a fallback
if (event.request.headers.get('accept')?.includes('text/html')) {
return caches.match('/offline.html')
}
return new Response('Network error occurred', {
status: 408,
headers: { 'Content-Type': 'text/plain' }
})
})
})
)
})
```
Register the service worker in the main application:
```typescript
// src/main.ts (modified)
// Register service worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('Service Worker registered with scope:', registration.scope)
})
.catch(error => {
console.error('Service Worker registration failed:', error)
})
})
}
```
#### 11.7 Create Offline Page
Create a simple offline page to be shown when the user is offline and tries to access a page that's not cached:
```html
<!-- public/offline.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Offline - SecureWeb</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
padding: 20px;
text-align: center;
background-color: #f9fafb;
color: #1e3a8a;
}
.icon {
font-size: 64px;
margin-bottom: 20px;
}
h1 {
margin-bottom: 10px;
}
p {
margin-bottom: 20px;
color: #4b5563;
max-width: 500px;
}
button {
background-color: #2563eb;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-weight: 500;
}
button:hover {
background-color: #1d4ed8;
}
</style>
</head>
<body>
<div class="icon">📡</div>
<h1>You're Offline</h1>
<p>The page you're trying to access isn't available offline. Please check your internet connection and try again.</p>
<button onclick="window.location.reload()">Try Again</button>
<script>
// Check if we're back online
window.addEventListener('online', () => {
window.location.reload()
})
</script>
</body>
</html>
```
### Offline Strategy Benefits
This comprehensive offline functionality implementation provides several key benefits:
1. **Seamless Offline Experience**: Users can continue browsing previously accessed content even when offline.
2. **Performance Improvements**: Cached content loads faster, reducing load times and bandwidth usage.
3. **Resilience**: The application gracefully handles network interruptions without disrupting the user experience.
4. **Reduced Network Dependency**: By caching IPFS content locally, the application reduces its dependency on the IPFS network.
5. **Transparent Status**: Users are always aware of their connection status and what content is available offline.
6. **Efficient Resource Usage**: The cache management strategy ensures efficient use of device storage while prioritizing recently accessed content.
```