Pre-AlphaMetaScript is in early design phase. The compiler is not yet available.Join Discord for updates
MetaScript

Create OTP Service

Use MetaScript's Erlang backend to build distributed, fault-tolerant systems with OTP supervision trees.

Why Erlang Backend?

  • Nine nines: 99.9999999% uptime proven in production
  • Hot code reload: Update code without dropping connections
  • Millions of processes: Lightweight concurrency
  • Distributed by default: Cluster multiple nodes seamlessly

Getting Started

Create OTP Project

npm create metascript@latest my-service -- --template otp
cd my-service

Project Structure

my-service/
├── src/
│   ├── main.ms           # Application entry
│   ├── supervisor.ms     # Supervision tree
│   └── workers/          # Worker processes
├── build.ms              # Build config
└── rebar.config          # Erlang build config

OTP Concepts in MetaScript

GenServer

A GenServer is a process that handles synchronous and asynchronous messages:

import { GenServer, call, cast } from "@metascript/otp"

@derive(GenServer)
class Counter {
  private count: number = 0

  // Synchronous call - caller waits for response
  @call
  increment(): number {
    this.count += 1
    return this.count
  }

  @call
  get(): number {
    return this.count
  }

  // Asynchronous cast - fire and forget
  @cast
  reset(): void {
    this.count = 0
  }
}

// Usage
const counter = await Counter.start()
console.log(await counter.increment()) // 1
console.log(await counter.increment()) // 2
counter.reset() // Returns immediately
console.log(await counter.get()) // 0

Supervisor

Supervisors manage child processes and restart them on failure:

import { Supervisor, RestartStrategy } from "@metascript/otp"

const supervisor = new Supervisor({
  strategy: RestartStrategy.OneForOne,
  maxRestarts: 3,
  maxSeconds: 5,
  children: [
    {
      id: "counter",
      start: () => Counter.start(),
      restart: "permanent",
    },
    {
      id: "cache",
      start: () => Cache.start({ maxSize: 1000 }),
      restart: "transient",
    },
  ],
})

await supervisor.start()

Restart Strategies

StrategyBehavior
OneForOneRestart only the failed child
OneForAllRestart all children when one fails
RestForOneRestart failed child and all after it

Building a Chat Server

A complete example of a distributed chat server:

User Process

import { GenServer, call, cast, info } from "@metascript/otp"

@derive(GenServer)
class UserSession {
  private socket: WebSocket
  private username: string
  private rooms: Set<string> = new Set()

  constructor(socket: WebSocket, username: string) {
    this.socket = socket
    this.username = username
  }

  @call
  joinRoom(room: string): void {
    this.rooms.add(room)
    RoomRegistry.join(room, this.pid)
  }

  @cast
  receiveMessage(from: string, message: string): void {
    this.socket.send(JSON.stringify({
      type: "message",
      from,
      message,
    }))
  }

  @info
  handleSocketMessage(data: string): void {
    const msg = JSON.parse(data)
    if (msg.type === "send") {
      RoomRegistry.broadcast(msg.room, this.username, msg.text)
    }
  }
}

Room Registry

import { GenServer, call } from "@metascript/otp"

@derive(GenServer)
class RoomRegistry {
  private rooms: Map<string, Set<Pid>> = new Map()

  @call
  join(room: string, pid: Pid): void {
    if (!this.rooms.has(room)) {
      this.rooms.set(room, new Set())
    }
    this.rooms.get(room)!.add(pid)
  }

  @call
  leave(room: string, pid: Pid): void {
    this.rooms.get(room)?.delete(pid)
  }

  @call
  broadcast(room: string, from: string, message: string): void {
    const members = this.rooms.get(room)
    if (members) {
      for (const pid of members) {
        pid.cast("receiveMessage", from, message)
      }
    }
  }
}

Application Supervisor

import { Application, Supervisor } from "@metascript/otp"

class ChatApp extends Application {
  start() {
    return new Supervisor({
      strategy: RestartStrategy.OneForOne,
      children: [
        {
          id: "room_registry",
          start: () => RoomRegistry.start(),
        },
        {
          id: "session_supervisor",
          start: () => SessionSupervisor.start(),
        },
        {
          id: "web_server",
          start: () => WebServer.start({ port: 8080 }),
        },
      ],
    })
  }
}

Hot Code Reload

Update running code without dropping connections:

# Build new release
msc build --target=erlang --release

# Hot upgrade
msc otp upgrade --node my_service@localhost
// Define upgrade path in your module
@derive(GenServer)
class Counter {
  // ...

  @codeChange
  upgrade(oldState: any, oldVersion: string): this {
    // Migrate state from old version
    return this
  }
}

Distributed Clustering

Connect Nodes

# Start first node
msc otp start --name node1@192.168.1.1

# Start second node and connect
msc otp start --name node2@192.168.1.2 --connect node1@192.168.1.1

Distributed Registry

import { global } from "@metascript/otp"

// Register process globally across cluster
await global.register("leader", LeaderProcess.start())

// Find process on any node
const leader = await global.whereis("leader")
await leader.call("doWork")

Partition Handling

import { net_kernel } from "@metascript/otp"

net_kernel.monitor((event) => {
  if (event.type === "nodedown") {
    console.log(`Node ${event.node} disconnected`)
    // Handle partition
  }
})

Deployment

Build Release

# Create production release
msc build --target=erlang --release

# Output in _build/prod/rel/my_service

Docker Deployment

FROM erlang:26-alpine

COPY _build/prod/rel/my_service /app
WORKDIR /app

CMD ["bin/my_service", "foreground"]

Kubernetes

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: my-service
spec:
  serviceName: my-service
  replicas: 3
  template:
    spec:
      containers:
        - name: my-service
          image: my-service:latest
          env:
            - name: ERLANG_COOKIE
              valueFrom:
                secretKeyRef:
                  name: erlang-secret
                  key: cookie

Next Steps