npm package: @ratio1/edge-node-client
Description: SDK for interacting with Ratio1 edge node services (CStore and R1FS)
Latest version: 1.1.4
Deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.

# edge-node-client

A comprehensive SDK for interacting with Ratio1 Edge Node services including **CStore** (distributed key-value store) and **R1FS** (distributed file system).

[![npm version](https://badge.fury.io/js/edge-node-client.svg)](https://badge.fury.io/js/edge-node-client)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Features

- 🔄 **Universal Compatibility**: Works in both Node.js and browser environments
- 🚀 **SSR Support**: Full Next.js Server-Side Rendering compatibility
- 📦 **Optimized Bundles**: Separate builds for Node.js and browser with minimal bundle size
- 🔧 **TypeScript Support**: Full TypeScript definitions included
- 🌐 **Environment Configuration**: Flexible configuration via environment variables or direct options
- 🛡️ **Error Handling**: Comprehensive error handling with detailed error messages
- 📁 **File Operations**: Complete file upload/download capabilities with base64 support
- 🔌 **Pluggable HTTP Layer**: Provide custom `fetch` and `FormData` implementations
- 🌐 **Protocol Flexibility**: URLs without a scheme automatically default to `http://`

## Installation

```bash
npm install @ratio1/edge-node-client
```

The SDK exports two factory functions:

- `createRatio1EdgeNodeClient` for Node.js environments
- `createRatio1EdgeNodeBrowserClient` for browsers or frameworks like Next.js

Both produce the same API surface so you can share code between environments.

## Quick Start

### Node.js Environment

```typescript
import createRatio1EdgeNodeClient from '@ratio1/edge-node-client'

const ratio1 = createRatio1EdgeNodeClient()

// Store a value (values must be stringified JSON)
await ratio1.cstore.setValue({ 
  key: 'user:123:preferences', 
  value: JSON.stringify({ theme: 'dark', language: 'en' })
})

// Retrieve the value
const result = await ratio1.cstore.getValue({ 
  key: 'user:123:preferences' 
})
console.log(JSON.parse(result.result)) // { theme: 'dark', language: 'en' }
```

### Browser Environment (Next.js, React, etc.)

```typescript
import { createRatio1EdgeNodeBrowserClient } from '@ratio1/edge-node-client/browser'

const ratio1 = createRatio1EdgeNodeBrowserClient()

// Use the same API as Node.js
const status = await ratio1.cstore.getStatus()
```

Both creation functions accept the same options. Use the `verbose` flag to enable debug logging.

```typescript
const ratio1 = createRatio1EdgeNodeClient({
  verbose: true
})
```

The SDK also lets you inject a custom `fetch` implementation and `FormData` constructor if needed.

## Configuration

### Environment Variables

The SDK automatically reads configuration from environment variables. These variables are typically injected into the container environment by the Edge Node deployment system:

**Node.js Environment:**
```bash
# These are automatically injected by the Edge Node container environment
export EE_CHAINSTORE_API_URL=http://localhost:31234
export EE_R1FS_API_URL=http://localhost:31235
```

### Next.js Configuration

Run in your application:

```typescript
import { createRatio1EdgeNodeBrowserClient } from '@ratio1/edge-node-client/browser'

const ratio1 = createRatio1EdgeNodeBrowserClient()
```

## API Reference

### CStore Client

CStore provides a distributed key-value store with hash operations.

**Important**: All values must be stringified JSON. The CStore service stores everything as JSON strings, so you need to use `JSON.stringify()` when setting values and `JSON.parse()` when retrieving them.

#### Basic Operations

```typescript
// Get service status
const status = await ratio1.cstore.getStatus()
// Returns: { success: true, result: true, keys?: string[] }

// Set a value (values must be stringified JSON)
await ratio1.cstore.setValue({ 
  key: 'my-key', 
  value: JSON.stringify('my-value')
})
// Returns: { success: true, result: true }

// Get a value
const result = await ratio1.cstore.getValue({ 
  key: 'my-key' 
})
// Returns: { success: true, result: '"my-value"' }
// Note: result is a JSON string, parse it to get the actual value
```

#### Hash Operations

```typescript
// Set a hash field (values must be stringified JSON)
await ratio1.cstore.hset({ 
  hkey: 'user:123', 
  key: 'name', 
  value: JSON.stringify('John Doe')
})

// Get a hash field
const name = await ratio1.cstore.hget({ 
  hkey: 'user:123', 
  key: 'name' 
})
// Returns: { success: true, result: '"John Doe"' }
// Note: result is a JSON string, parse it to get the actual value

// Get all hash fields
const allFields = await ratio1.cstore.hgetall({ 
  hkey: 'user:123' 
})
// Returns: { success: true, result: { keys: ['name', 'email', 'age'] } }
```

#### Advanced Usage Examples

```typescript
// Store complex objects (must be stringified)
await ratio1.cstore.setValue({
  key: 'user:123:profile',
  value: JSON.stringify({
    name: 'John Doe',
    email: 'john@example.com',
    preferences: {
      theme: 'dark',
      notifications: true
    }
  })
})

// Store arrays (must be stringified)
await ratio1.cstore.setValue({
  key: 'user:123:posts',
  value: JSON.stringify(['post1', 'post2', 'post3'])
})

// Hash operations for user data (values must be stringified)
await ratio1.cstore.hset({ hkey: 'user:123', key: 'name', value: JSON.stringify('John') })
await ratio1.cstore.hset({ hkey: 'user:123', key: 'email', value: JSON.stringify('john@example.com') })
await ratio1.cstore.hset({ hkey: 'user:123', key: 'age', value: JSON.stringify(30) })

// Retrieve all user data
const userData = await ratio1.cstore.hgetall({ hkey: 'user:123' })
```

### R1FS Client

R1FS provides a distributed file system with upload and download capabilities.

#### File Upload

```typescript
// Upload a file using FormData (browser)
const fileInput = document.getElementById('file') as HTMLInputElement
const file = fileInput.files[0]

const formData = new FormData()
formData.append('file', file)
formData.append('filename', file.name)
formData.append('secret', 'optional-secret-key')

const uploadResult = await ratio1.r1fs.addFile({ formData })
// Returns: { success: true, result: { message: 'File uploaded', cid: 'QmHash...' } }

// Upload using base64 (works in both Node.js and browser)
const base64Data = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...'
const uploadResult = await ratio1.r1fs.addFileBase64({
  file_base64_str: base64Data,
  filename: 'image.png',
  secret: 'optional-secret-key'
})
```

#### Streaming Uploads (Node.js & Next.js)

```typescript
import fs from 'node:fs'
import createRatio1EdgeNodeClient from '@ratio1/edge-node-client'

const ratio1 = createRatio1EdgeNodeClient()

const stream = fs.createReadStream('/tmp/report.csv')

const upload = await ratio1.r1fs.addFile({
  file: stream,
  filename: 'report.csv',
  contentType: 'text/csv',
  secret: process.env.R1FS_SECRET
})

console.log(upload.cid)
```

```typescript
// app/api/upload/route.ts (Next.js App Router)
import { NextRequest, NextResponse } from 'next/server'
import { Readable } from 'node:stream'
import createRatio1EdgeNodeClient from '@ratio1/edge-node-client'

export async function POST (req: NextRequest) {
  const form = await req.formData()
  const file = form.get('file')

  if (!(file instanceof File)) {
    return NextResponse.json({ error: 'file field missing' }, { status: 400 })
  }

  const ratio1 = createRatio1EdgeNodeClient()
  const readable = Readable.fromWeb(file.stream())

  const result = await ratio1.r1fs.addFile({
    file: readable,
    filename: file.name,
    contentType: file.type,
    secret: form.get('secret')?.toString()
  })

  return NextResponse.json({ cid: result.cid })
}
```

#### File Download

```typescript
// Download file as blob/response
const fileResponse = await ratio1.r1fs.getFile({ 
  cid: 'QmHash...',
  secret: 'optional-secret-key' // if file was uploaded with secret
})

// For browser: create download link
const blob = await fileResponse.blob()
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'filename.ext'
a.click()

// Download as base64
const base64Result = await ratio1.r1fs.getFileBase64({ 
  cid: 'QmHash...',
  secret: 'optional-secret-key'
})
// Returns: { file_base64_str: 'data:image/png;base64,...', filename: 'image.png' }
```

#### Service Status

```typescript
// Check R1FS service status
const status = await ratio1.r1fs.getStatus()
// Returns: { EE_ID: 'edge-node-id', ... }
```

#### Complete File Management Example

```typescript
// Upload multiple files
async function uploadFiles(files: File[]) {
  const uploadPromises = files.map(async (file) => {
    const formData = new FormData()
    formData.append('file', file)
    formData.append('filename', file.name)
    
    return await ratio1.r1fs.addFile({ formData })
  })
  
  const results = await Promise.all(uploadPromises)
  return results.map(r => r.result.cid)
}

// Download and display image
async function displayImage(cid: string) {
  const response = await ratio1.r1fs.getFile({ cid })
  const blob = await response.blob()
  const url = URL.createObjectURL(blob)
  
  const img = document.createElement('img')
  img.src = url
  document.body.appendChild(img)
}
```

## Error Handling

The SDK provides comprehensive error handling:

```typescript
try {
  const result = await ratio1.cstore.getValue({ key: 'non-existent-key' })
  console.log(result.result)
} catch (error) {
  if (error.message.includes('404')) {
    console.log('Key not found')
  } else if (error.message.includes('500')) {
    console.log('Server error')
  } else {
    console.log('Network error:', error.message)
  }
}
```

## Bundle Size Optimization

The package is optimized for different environments:

| Environment | Bundle Size | Dependencies |
|-------------|-------------|--------------|
| **Node.js** | ~50KB | Includes cross-fetch, form-data |
| **Browser** | ~15KB | Uses native APIs only |

### Tree Shaking

The package is marked as `sideEffects: false`, enabling optimal tree shaking:

```typescript
// Only CStore functionality will be included
import { CStoreClient } from '@ratio1/edge-node-client'

// Only R1FS functionality will be included  
import { R1FSClient } from '@ratio1/edge-node-client'
```

## Development

### Setup

```bash
# Clone the repository
git clone <repository-url>
cd @ratio1/edge-node-client

# Install dependencies
npm install

# Build for both Node.js and browser
npm run build

# Run tests
npm test

# Run specific test suites
npm run test:e2e:cstore
npm run test:e2e:r1fs
```

### Testing

```bash
# Run all tests
npm test

# Run specific test files
npm test -- src/__tests__/cstoreClient.test.ts
npm test -- src/__tests__/r1fsClient.test.ts

# Run end-to-end tests
npm run test:e2e:cstore
npm run test:e2e:r1fs
```

## Examples

### Next.js Application

```typescript
// pages/api/upload.ts
import createRatio1EdgeNodeClient from '@ratio1/edge-node-client'

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' })
  }

  const ratio1 = createRatio1EdgeNodeClient()

  try {
    // Store metadata (must be stringified)
await ratio1.cstore.setValue({
  key: `upload:${Date.now()}`,
  value: JSON.stringify({
    filename: req.body.filename,
    size: req.body.size,
    uploadedAt: new Date().toISOString()
  })
})

    res.status(200).json({ success: true })
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
}
```

### React Component

```typescript
// components/FileUpload.tsx
import { createBrowserClient } from '@ratio1/edge-node-client/browser'
import { useState } from 'react'

export function FileUpload() {
  const [uploading, setUploading] = useState(false)
  const [uploadedFiles, setUploadedFiles] = useState<string[]>([])

  const ratio1 = createBrowserClient()

  const handleUpload = async (files: FileList) => {
    setUploading(true)
    
    try {
      const uploadPromises = Array.from(files).map(async (file) => {
        const formData = new FormData()
        formData.append('file', file)
        formData.append('filename', file.name)
        
        const result = await ratio1.r1fs.addFile({ formData })
        return result.result.cid
      })
      
      const cids = await Promise.all(uploadPromises)
      setUploadedFiles(prev => [...prev, ...cids])
    } catch (error) {
      console.error('Upload failed:', error)
    } finally {
      setUploading(false)
    }
  }

  return (
    <div>
      <input
        type="file"
        multiple
        onChange={(e) => e.target.files && handleUpload(e.target.files)}
        disabled={uploading}
      />
      {uploading && <p>Uploading...</p>}
      {uploadedFiles.length > 0 && (
        <div>
          <h3>Uploaded Files:</h3>
          <ul>
            {uploadedFiles.map((cid, index) => (
              <li key={index}>{cid}</li>
            ))}
          </ul>
        </div>
      )}
    </div>
  )
}
```

## Browser Compatibility

- **Modern Browsers**: Chrome 60+, Firefox 55+, Safari 11+, Edge 79+
- **Node.js**: 14.0+
- **Next.js**: 12.0+
- **React**: 16.8+

## License

MIT License - see [LICENSE](LICENSE) file for details.

## Contributing

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

## Support

- 📖 **Documentation**: This README and inline code comments
- 🐛 **Issues**: [GitHub Issues](https://github.com/your-repo/@ratio1/edge-node-client/issues)
- 💬 **Discussions**: [GitHub Discussions](https://github.com/your-repo/@ratio1/edge-node-client/discussions)