# Creating Acequia Apps

This guide walks through building a new Acequia-powered app. It covers directory structure, initialization, route registration, peer communication, and common patterns.

## App Structure

Apps live in `acequia/{subdomain}/YourApp/`. The subdomain is determined by the host serving the files (e.g., `stigmergic.acequia.live` → `stigmergic`).

### Minimal App

```
acequia/{subdomain}/MyApp/
├── index.html
└── src/
    └── main.js
```

### Typical App

```
acequia/{subdomain}/MyApp/
├── index.html
├── src/
│   ├── main.js                    # Entry point
│   ├── modules/
│   │   └── acequia-integration.js # Acequia setup & routes
│   ├── components/                # Web components (optional)
│   └── styles/                    # CSS (optional)
```

## Bootstrap Pattern

### HTML Template

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Acequia App</title>
</head>
<body>
    <div id="app">
        <h1>My App</h1>
        <div id="status"></div>
    </div>

    <script type="module" src="src/main.js"></script>
</body>
</html>
```

### Main Entry Point

```javascript
// src/main.js
import acequia from '/acequia.js'

class MyApp {
    constructor() {
        this.group = null
    }

    async initialize() {
        // 1. Wait for acequia framework to be ready
        const { deviceId, userId } = await acequia.acequiaReady()
        console.log('Acequia ready, deviceId:', deviceId)

        // 2. Read URL parameters
        const params = new URLSearchParams(window.location.search)
        const groupName = params.get('group') || 'default'
        const displayName = params.get('name') || acequia.utils.generateRandomName()

        // 3. Create and join group
        this.group = new acequia.groups.Group(groupName, {
            displayName,
            capabilities: ['myapp'],
            onPeersList: (peers) => {
                console.log('Peers updated:', peers)
            },
        })
        await this.group.ready
        console.log(`Joined group "${groupName}" as "${displayName}"`)

        // 4. Register routes
        this.registerRoutes()
    }

    registerRoutes() {
        const instanceId = acequia.getInstanceId()

        this.group.registerRoutes([
            {
                pattern: `/${instanceId}/status`,
                handler: async (request) => ({
                    status: 200,
                    body: JSON.stringify({
                        online: true,
                        timestamp: new Date().toISOString(),
                    }),
                    headers: { 'content-type': 'application/json' },
                }),
            },
        ])
    }
}

// Initialize
const app = new MyApp()
app.initialize().catch(console.error)
```

## Acequia Integration

### Importing Acequia

**Static import** (recommended):
```javascript
import acequia from '/acequia.js'
```

**Dynamic import** (if needed for lazy loading):
```javascript
const acequiaModule = await import('/acequia.js')
const acequia = acequiaModule.default
```

The `/acequia.js` path works because the service worker is registered at `/` scope, and the file is served by localWebDAV.

### Initialization Sequence

```javascript
// Must await this before using any acequia APIs
const { deviceId, userId, publicJWK } = await acequia.acequiaReady()
```

`acequiaReady()` resolves when:
1. Device/user IDs are generated or loaded from IndexedDB
2. Cryptographic keys are ready
3. Service worker is registered and controlling the page

### Creating a Group

```javascript
const group = new acequia.groups.Group(groupName, {
    // Required
    displayName: 'My Display Name',     // Shown in peer lists

    // Optional
    discoveryURL: '',                    // '' = same origin (default)
    capabilities: ['camera', 'editor'], // Advertised capabilities
    onPeersList: (peers) => {},         // Callback when peer list changes
    onOpen: (event) => {},              // WebSocket connected
    onClose: (event) => {},             // WebSocket disconnected
    onError: (event, err) => {},        // Error occurred
})

// Wait for WebSocket connection and welcome message
await group.ready
```

**Important:** Always pass `displayName`. Without it, the peer shows as undefined in peer lists.

### Group URL

The group and display name are typically passed as URL parameters:

```
https://stigmergic.acequia.live/MyApp/?group=mygroup&name=MyDevice
```

## Route Registration

### Single Route

```javascript
group.registerRoute('/path', handler, metadata)
```

### Batch Registration (Recommended)

Use `registerRoutes()` to register all routes in a single WebSocket message:

```javascript
group.registerRoutes([
    { pattern: '/status', handler: statusHandler },
    { pattern: '/data', handler: dataHandler },
    { pattern: '/config', handler: configHandler },
])
```

This sends one WS message and one broadcast instead of N. Always prefer batch registration.

### Route Patterns

Patterns support parameters and wildcards:

| Pattern | Matches | Extracted Params |
|---------|---------|-----------------|
| `/status` | `/status` | `{}` |
| `/users/:id` | `/users/123` | `{ id: '123' }` |
| `/files/*path` | `/files/a/b/c.txt` | `{ path: 'a/b/c.txt' }` |

Routes are typically prefixed with `instanceId` to avoid collisions when multiple instances of the same app are in a group:

```javascript
const instanceId = acequia.getInstanceId()
group.registerRoute(`/${instanceId}/shot`, handler)
```

### Route Handler Format

Handlers receive a request object and must return a response object:

```javascript
async function handler(request) {
    // request contains:
    // - method: 'GET', 'POST', 'PUT', etc.
    // - path: the matched URL path
    // - pathParams: extracted route parameters
    // - query: parsed query string
    // - headers: request headers
    // - body: request body (string, array of bytes, or object)
    // - cookies: parsed cookies

    return {
        status: 200,                              // HTTP status code
        statusText: 'OK',                         // Optional status text
        headers: { 'content-type': 'text/plain' }, // Response headers
        body: 'Hello World',                       // Response body
    }
}
```

### Response Body Types

- **String** — sent as-is
- **JSON string** — use `JSON.stringify()` and set `content-type: application/json`
- **Base64 binary** — set `bodyEncoding: 'base64'` for binary data (images, files)

```javascript
// JSON response helper
function jsonResponse(status, data) {
    return {
        status,
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify(data),
    }
}

// Binary response (e.g., image)
return {
    status: 200,
    headers: { 'content-type': 'image/jpeg' },
    body: base64EncodedData,
    bodyEncoding: 'base64',
}
```

### Accessing Routes

Once registered, routes are accessible via HTTP:

```
GET /groups/{groupId}/{instanceId}/status
```

From JavaScript:
```javascript
const response = await fetch(`/groups/mygroup/${instanceId}/status`)
const data = await response.json()
```

From curl:
```bash
curl "https://stigmergic.acequia.live/groups/mygroup/abc123/status?token=z0lx3ktec7a8f3smvxkhodst"
```

## Common Patterns

### State API Pattern

Expose app state via GET/PUT routes, similar to Scribbler:

```javascript
let appState = { items: [], lastModified: null }

group.registerRoutes([
    {
        pattern: `/${instanceId}/state`,
        handler: async (request) => {
            if (request.method === 'PUT' || request.method === 'POST') {
                const body = parseBody(request)
                appState = { ...appState, ...body, lastModified: new Date().toISOString() }
                notifySubscribers(appState)
                return jsonResponse(200, { success: true, state: appState })
            }
            return jsonResponse(200, appState)
        },
    },
])
```

### Webhook Subscription Pattern

Let remote clients subscribe to state changes:

```javascript
const subscribers = new Map()

group.registerRoutes([
    {
        pattern: `/${instanceId}/subscribe`,
        handler: async (request) => {
            const { webhookUrl } = parseBody(request)
            const id = crypto.randomUUID()
            subscribers.set(id, { webhookUrl, subscribedAt: new Date().toISOString() })
            return jsonResponse(200, { subscriptionId: id })
        },
    },
    {
        pattern: `/${instanceId}/unsubscribe`,
        handler: async (request) => {
            const { subscriptionId } = parseBody(request)
            subscribers.delete(subscriptionId)
            return jsonResponse(200, { removed: true })
        },
    },
])

// When state changes, notify all subscribers
async function notifySubscribers(state) {
    for (const [id, sub] of subscribers) {
        try {
            await fetch(sub.webhookUrl, {
                method: 'PUT',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ subscriptionId: id, state }),
            })
        } catch (err) {
            console.error(`Webhook failed for ${sub.webhookUrl}:`, err)
        }
    }
}
```

### Parsing Request Bodies

Request bodies arrive in different formats depending on the transport. Use a helper:

```javascript
function parseBody(request) {
    const { body } = request
    if (!body) return null

    let text
    if (typeof body === 'string') {
        text = body.trim()
    } else if (Array.isArray(body)) {
        text = new TextDecoder().decode(new Uint8Array(body))
    } else if (body instanceof Uint8Array || body instanceof ArrayBuffer) {
        text = new TextDecoder().decode(new Uint8Array(body))
    } else {
        return body // Already parsed object
    }

    return JSON.parse(text)
}
```

## WebRTC Peer Communication

Beyond the HTTP-style route pattern, Acequia supports direct WebRTC connections for streaming and low-latency data.

### Handling New Peers

```javascript
acequia.webrtc.onNewPeer((peer) => {
    console.log('New peer:', peer.peerId)

    // Receive large/chunked data
    peer.on('dataBig', (data) => {
        console.log('Data from peer:', data)
    })

    peer.on('close', () => {
        console.log('Peer disconnected:', peer.peerId)
    })
})
```

### Sending Data to Peers

```javascript
const peers = acequia.webrtc.getPeers()
for (const [peerId, peer] of Object.entries(peers)) {
    peer.sendBig({ type: 'stateUpdate', data: appState })
}
```

`sendBig()` automatically chunks large payloads (>16KB) and reassembles on the other side.

### Adding Media Streams

For camera/audio streaming between peers:

```javascript
// Sender: add stream to peer connection
const stream = await navigator.mediaDevices.getUserMedia({ video: true })
for (const track of stream.getTracks()) {
    peer._pc.addTrack(track, stream) // Access underlying RTCPeerConnection
}

// Receiver: listen for tracks
peer._pc.ontrack = (event) => {
    videoElement.srcObject = event.streams[0]
}
```

## Configuration

### Client Config

The service worker reads `config.js` on initialization:

```javascript
// public/config.js
import { config as localConfig } from './config.local.js'

export const config = {
    debug: true,
    dataVersion: 'v0019.9999',
    cacheSource: false,
    acequia: {
        authURL: '/auth',
        discoveryURL: '',    // Empty = same origin
    },
    alwaysFetch: ['/'],      // Always fetch from network
    ...localConfig,
}
```

Key settings:
- `discoveryURL: ''` — Discovery server is on the same origin (Cloudflare tunnel handles routing)
- `alwaysFetch: ['/']` — Pattern `/` matches all URLs via `String.search()`, so all requests go to network
- `cacheSource: false` — Don't cache app files (useful during development)

### URL Parameters

Apps typically accept these URL parameters:

| Parameter | Purpose | Example |
|-----------|---------|---------|
| `group` | Group name to join | `?group=camera` |
| `name` | Display name in group | `?name=MyDevice` |
| `token` | Auth token (for curl access) | `?token=z0lx3ktec7a8f3smvxkhodst` |

## Common Pitfalls

### Missing displayName

If you don't pass `displayName` to the Group constructor, your peer appears as `undefined` in peer lists. Always set it:

```javascript
new acequia.groups.Group(groupName, {
    displayName: 'My App',  // Required!
    ...
})
```

### Individual vs Batch Route Registration

Each `registerRoute()` call sends a separate WebSocket message and triggers a separate broadcast. For apps with many routes, use `registerRoutes()` instead:

```javascript
// Bad: 10 WS messages, 10 broadcasts
routes.forEach(r => group.registerRoute(r.pattern, r.handler))

// Good: 1 WS message, 1 broadcast
group.registerRoutes(routes)
```

### Module Import Failures

If ANY static import in your module tree fails (e.g., a file doesn't exist), the entire module tree fails silently. Check the browser console for import errors.

### Service Worker Cache

During development, the SW may serve cached versions of your files. Either:
- Set `cacheSource: false` in config
- Use DevTools > Application > Service Workers > "Update on reload"
- Add your app paths to `alwaysFetch` in config

### Binary Data Over Routes

Route responses travel over WebSocket (text protocol). For binary data, base64-encode it:

```javascript
const arrayBuffer = await blob.arrayBuffer()
const uint8Array = new Uint8Array(arrayBuffer)
const base64 = btoa(String.fromCharCode(...uint8Array))

return {
    status: 200,
    headers: { 'content-type': blob.type },
    body: base64,
    bodyEncoding: 'base64',
}
```

For very large binary data, use WebRTC `peer.sendBig()` instead.

### CORS Headers

Routes accessed from external origins need CORS headers:

```javascript
function addCorsHeaders(headers = {}) {
    return {
        ...headers,
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    }
}
```

Handle OPTIONS preflight in every route:
```javascript
if (request.method === 'OPTIONS') {
    return { status: 200, headers: addCorsHeaders(), body: '' }
}
```

## Working Examples

### Scribbler App

Location: `acequia/stigmergic/Scribbler/`

A collaborative drawing app that demonstrates:
- Static `import acequia from '/acequia.js'`
- State API pattern (GET/PUT state)
- Webhook subscriptions for state change notifications
- Peer discovery with capability filtering
- Batch route registration

### Camera App

Location: `acequia/stigmergic/Camera/`

A remote camera control app that demonstrates:
- Dynamic `import('/acequia.js')`
- Image capture and base64 encoding for route responses
- WebRTC media stream sharing
- Individual setting routes with GET/PUT
- Webhook notifications for settings changes
- UI visibility remote control

## Testing Your App

### Access via Browser

Navigate to:
```
https://{subdomain}.acequia.live/YourApp/?group=test&name=dev1
```

### Access via curl

```bash
# Get a route response
curl "https://stigmergic.acequia.live/groups/test/{instanceId}/status?token=YOUR_TOKEN"

# Check registered routes
curl "https://stigmergic.acequia.live/admin/routes/test"

# Check connected peers
curl "https://stigmergic.acequia.live/admin/clients/test"
```

### Multi-Tab Testing

Open your app in two tabs with different names:
```
Tab 1: /YourApp/?group=test&name=device1
Tab 2: /YourApp/?group=test&name=device2
```

Both tabs should see each other in the peers list. Routes registered by one should be callable from the other.

### Cross-Browser Testing

Open in Chrome and Safari simultaneously. WebRTC peer connections establish automatically for direct communication.