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

37 KiB

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

# 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:

// 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:

// 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:

<!-- 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:

<!-- 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:

// 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:

<!-- 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:

// 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:

<!-- 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

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:

// 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:

// 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:

// 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:

<!-- 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:

<!-- 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:

// 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:

// 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:

<!-- 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.