npm package: @ratio1/edge-sdk-ts
Description: SDK for interacting with Ratio1 edge node services (CStore and R1FS)
Latest version: 1.5.2

# @ratio1/edge-sdk-ts

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/@ratio1%2Fedge-sdk-ts.svg)](https://badge.fury.io/js/@ratio1%2Fedge-sdk-ts)
[![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-sdk-ts
```

The SDK exports two factory functions:

- `createEdgeSdk` for Node.js environments
- `createEdgeSdkBrowserClient` 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 createEdgeSdk from '@ratio1/edge-sdk-ts'

const ratio1 = createEdgeSdk()

// 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 { createEdgeSdkBrowserClient } from '@ratio1/edge-sdk-ts/browser'

const ratio1 = createEdgeSdkBrowserClient()

// 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 = createEdgeSdk({
  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 { createEdgeSdkBrowserClient } from '@ratio1/edge-sdk-ts/browser'

const ratio1 = createEdgeSdkBrowserClient()
```

## 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 createEdgeSdk from '@ratio1/edge-sdk-ts'

const ratio1 = createEdgeSdk()

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 createEdgeSdk from '@ratio1/edge-sdk-ts'

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 = createEdgeSdk()
  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-sdk-ts'

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

## Development

### Setup

```bash
# Clone the repository
git clone <repository-url>
cd edge-sdk-ts

# 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 createEdgeSdk from '@ratio1/edge-sdk-ts'

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

  const ratio1 = createEdgeSdk()

  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 { createEdgeSdkBrowserClient } from "@ratio1/edge-sdk-ts/browser";
import { useState } from "react";

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

	const ratio1 = createEdgeSdkBrowserClient();

	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/Ratio1/edge-sdk-ts/issues)