OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-rooms / examples / cli.js

231 lines · javascript ref:
#!/usr/bin/env node
/**
 * Mini CLI example for hyper-rooms
 *
 * Demonstrates a Room with a tiny "todos" schema. Lets you create rooms,
 * append todos, list them, and manage writers.
 *
 * Usage:
 *   node examples/cli.js init                                Create a new room
 *   node examples/cli.js info                                Show room key + writers
 *   node examples/cli.js add <text>                          Add a todo
 *   node examples/cli.js list                                List todos
 *   node examples/cli.js check <id>                          Mark todo done
 *   node examples/cli.js del <id>                            Delete todo
 *   node examples/cli.js add-writer <userId> <writerKey>     Add another writer
 *   node examples/cli.js list-writers                        List writers
 *   node examples/cli.js demo                                Run a full demo
 *
 * Storage: HYPER_ROOMS_STORE env var (default: ./.hyper-rooms-cli)
 *
 * NOTE: this CLI uses its own storage path. The web example uses a different
 * default. They don't share state — RocksDB single-writer lock. For shared
 * state across CLI/web/native interfaces in production, embed hyper-rooms in
 * a single daemon and have all clients talk to the daemon via hyper-svc
 * (planned).
 */

import Corestore from 'corestore'
import { Room, jsonSchema } from '../index.js'

const storagePath = process.env.HYPER_ROOMS_STORE || './.hyper-rooms-cli'
const args = process.argv.slice(2)
const cmd = args[0]

const todosSchema = jsonSchema({
  async apply (batch, op, host) {
    if (op.type === 'add-todo') {
      await batch.put(`todo:${op.id}`, {
        id: op.id,
        text: op.text,
        done: false,
        createdAt: op.timestamp
      })
      // Bump the counter
      const counter = await batch.get('meta:next-id')
      await batch.put('meta:next-id', { value: Math.max(counter?.value?.value || 1, op.id + 1) })
    } else if (op.type === 'check-todo') {
      const e = await batch.get(`todo:${op.id}`)
      if (e) await batch.put(`todo:${op.id}`, { ...e.value, done: true })
    } else if (op.type === 'delete-todo') {
      await batch.del(`todo:${op.id}`)
    }
  }
})

async function open () {
  const store = new Corestore(storagePath)
  await store.ready()
  const room = new Room(store, { schema: todosSchema, namespace: 'todos' })
  await room.ready()
  return { room, store }
}

async function close ({ room, store }) {
  await room.close()
  await store.close()
}

async function nextId (room) {
  const counter = await room.view.get('meta:next-id')
  return counter ? counter.value.value : 1
}

async function main () {
  if (cmd === 'init') {
    const ctx = await open()
    console.log('Room initialized at: %s', storagePath)
    console.log('  key:      %s', ctx.room.keyHex)
    console.log('  localKey: %s', ctx.room.localKeyHex)
    console.log('  writable: %s', ctx.room.writable)
    await close(ctx)
    return
  }

  if (cmd === 'info') {
    const ctx = await open()
    console.log('Room: %s', storagePath)
    console.log('  key:      %s', ctx.room.keyHex)
    console.log('  localKey: %s', ctx.room.localKeyHex)
    console.log('  writable: %s', ctx.room.writable)
    const writers = await ctx.room.listWriters()
    console.log('  writers:  %d', writers.length)
    for (const w of writers) {
      console.log('    %s (%s) %s', w.userId, w.role, (w.writerKey || '').slice(0, 16) + '...')
    }
    await close(ctx)
    return
  }

  if (cmd === 'add') {
    const text = args.slice(1).join(' ')
    if (!text) { console.error('Usage: cli.js add <text>'); process.exit(1) }
    const ctx = await open()
    if (!ctx.room.writable) {
      console.error('Room is not writable from this peer (no writer key registered).')
      await close(ctx)
      process.exit(1)
    }
    const id = await nextId(ctx.room)
    await ctx.room.append({ type: 'add-todo', id, text, timestamp: Date.now() })
    await ctx.room.base.update()
    console.log('Added todo #%d: %s', id, text)
    await close(ctx)
    return
  }

  if (cmd === 'list') {
    const ctx = await open()
    const todos = []
    for await (const entry of ctx.room.view.createReadStream({
      gte: 'todo:', lt: 'todo:\xff'
    })) {
      todos.push(entry.value)
    }
    if (todos.length === 0) {
      console.log('(no todos)')
    } else {
      for (const t of todos) {
        console.log('  [%s] #%d %s', t.done ? 'x' : ' ', t.id, t.text)
      }
      console.log('\n%d todos', todos.length)
    }
    await close(ctx)
    return
  }

  if (cmd === 'check') {
    const id = parseInt(args[1])
    if (!id) { console.error('Usage: cli.js check <id>'); process.exit(1) }
    const ctx = await open()
    await ctx.room.append({ type: 'check-todo', id })
    await ctx.room.base.update()
    console.log('Checked #%d', id)
    await close(ctx)
    return
  }

  if (cmd === 'del') {
    const id = parseInt(args[1])
    if (!id) { console.error('Usage: cli.js del <id>'); process.exit(1) }
    const ctx = await open()
    await ctx.room.append({ type: 'delete-todo', id })
    await ctx.room.base.update()
    console.log('Deleted #%d', id)
    await close(ctx)
    return
  }

  if (cmd === 'add-writer') {
    const userId = args[1]
    const writerKey = args[2]
    if (!userId || !writerKey) {
      console.error('Usage: cli.js add-writer <userId> <writerKey>')
      process.exit(1)
    }
    const ctx = await open()
    await ctx.room.addWriter(userId, { writerKey, role: 'writer' })
    await ctx.room.base.update()
    console.log('Added writer %s (%s)', userId, writerKey.slice(0, 16) + '...')
    await close(ctx)
    return
  }

  if (cmd === 'list-writers') {
    const ctx = await open()
    const writers = await ctx.room.listWriters()
    if (writers.length === 0) {
      console.log('(no writers)')
    } else {
      for (const w of writers) {
        console.log('  %s (%s) writerKey=%s addedBy=%s', w.userId, w.role, (w.writerKey || '').slice(0, 16) + '...', w.addedBy || '?')
      }
    }
    await close(ctx)
    return
  }

  if (cmd === 'demo') {
    const ctx = await open()
    console.log('--- hyper-rooms todos demo ---\n')
    console.log('Room key:  %s', ctx.room.keyHex)
    console.log('Local key: %s\n', ctx.room.localKeyHex)

    console.log('1. Add three todos')
    await ctx.room.append({ type: 'add-todo', id: 1, text: 'buy milk', timestamp: Date.now() })
    await ctx.room.append({ type: 'add-todo', id: 2, text: 'walk dog', timestamp: Date.now() })
    await ctx.room.append({ type: 'add-todo', id: 3, text: 'write code', timestamp: Date.now() })
    await ctx.room.base.update()

    console.log('2. Check off "walk dog"')
    await ctx.room.append({ type: 'check-todo', id: 2 })
    await ctx.room.base.update()

    console.log('3. List todos:')
    for await (const entry of ctx.room.view.createReadStream({ gte: 'todo:', lt: 'todo:\xff' })) {
      const t = entry.value
      console.log('   [%s] #%d %s', t.done ? 'x' : ' ', t.id, t.text)
    }

    await close(ctx)
    return
  }

  console.log('hyper-rooms example CLI (todos schema)')
  console.log('  init                            Create a new room')
  console.log('  info                            Show room key + writers')
  console.log('  add <text>                      Add a todo')
  console.log('  list                            List todos')
  console.log('  check <id>                      Mark todo done')
  console.log('  del <id>                        Delete todo')
  console.log('  add-writer <userId> <key>       Authorize another writer')
  console.log('  list-writers                    List authorized writers')
  console.log('  demo                            Run a full demo')
  console.log('')
  console.log('Storage: HYPER_ROOMS_STORE env var (default: ./.hyper-rooms-cli)')
  console.log('Note: this CLI does NOT share state with examples/web.js — they use')
  console.log('different storage paths because RocksDB only allows one writer.')
}

main().catch((err) => { console.error(err.message); process.exit(1) })