Download Game! Currently 86 players and visitors. Last logged in:CilwandNafleinMagiscusPere

Blitzer's Blog >> 72465

Back to blogs index
Posted: 15 Sep 2026 16:13 [ permalink ]
# Async VM Command Microservice  Architecture & Code

## Architecture Overview

vm-command-service/
 package.json
 server.js                 # Express wiring, execution route, boot sequence
 lib/
    commandLoader.js      # Load/unload command modules, register w/ control
plane
    watcher.js            # Debounced fs.watch wrapper for hot-reload
 commands/                 # Drop-in command modules live here
     pray.js

**Data flow**
**Data flow**
   .js   lib/watcher.js (fs.watch) file :
   added/changed    ,
 debounced event                                    
                                    <
                                    <
  lib/commandLoader.js                         
  - bust require.cache                         
  - validate module                            
  - registry.set(verb,&)                       
                    ,
 POST /register                                    
                                    <
                          VM Control Plane (:8080)

   Player action : POST /api/commands/:verb : registry lookup : execute(ctx)
                                                          
                                                          <
                                              { output, mutations } : VM

**Design principles**

| Concern | Approach |
|---|---|
| Loading | `fs.readdirSync` + `require()`, one file = one verb |
| Reload | Native `fs.watch` on `./commands` with per-file debounce |
| Cache busting | `delete require.cache[require.resolve(p)]` before re-require
|
| Registration | Fire-and-forget POST to control plane; errors logged, not
fatal |
| Execution | Single dynamic route `/api/commands/:verb`, delegates to module
|
| Safety | Per-command timeout via `Promise.race`; failures return `fail_msg`
|
| Identity | Registration endpoint is derived from verb  no per-file config |

---

## `package.json`

> Requires Node % 18 (uses built-in `fetch`).

## `lib/commandLoader.js`
## `lib/watcher.js`

> **Portability note:** `fs.watch` is fine on Linux (inotify). If you deploy
on macOS/Windows or edit files via tools that do atomic rename+replace on some
filesystems, swap in [`chokidar`](https://github.com/paulmillr/chokidar)  it
exposes an `awaitWriteFinish` option that eliminates partial-read races. The
rest of the code is unchanged.

## `server.js`
## `commands/pray.js`  example module

## How the loop behaves end-to-end

**1. First boot**

$ npm start
[loader] registered PRAY -> http://localhost:3000/api/commands/pray
[server] listening on 3000
[server] watching /app/commands

The control plane receives:

{
  "verb": "PRAY",
  "endpoint": "http://localhost:3000/api/commands/pray",
  "start_msg": "You bow your head and begin to pray...",
  "timeout": 2000,
  "fail_msg": "Your prayers echo into the void. Nothing answers."
}

**2. A player prays**

POST /api/commands/pray
{ "player": {"id":"plr_1","blessings":0},
  "room":   {"id":"temple"},
  "inventory": [{"id":"stone_idol"}] }

Response:

{
  "ok": true,
  "output": "The stone idol grows warm in your hands...",
  "mutations": {
    "memory": { "id": "plr_1", "blessings": 1, "last_blessed_at":
1717098423123 },
    "room":   { "id": "temple", "ambient": "A faint thread of incense
lingers..." }
  }
}

**3. Dropping in a new verb from your shell bot**

scp shout.js deploy@vm:/app/commands/

Within ~200 ms (debounce), the service logs:

[loader] registered SHOUT -> http://localhost:3000/api/commands/shout
[hot-reload] shout.js

&and the control plane is auto-notified. Editing `pray.js` in place triggers
the same cycle: cache is busted, the module re-evaluated, the payload
re-POSTed  **without restarting Express**.

---

## Extension points worth wiring next

1. **Unregister on delete.** Right now `onDelete` only drops the local entry.
Add a `POST /unregister` to the control plane so it can prune dead verbs.
2. **Schema validation of `ctx`.** A per-command `inputSchema` (Zod, Joi)
validated in the execution route keeps command logic defensive-free.
3. **Generation counter / versioning.** Include a monotonic `version` in the
registration payload so the control plane can ignore out-of-order reloads
during flurries.
4. **Swap `fs.watch`  `chokidar`** if you ever deploy to macOS or write files
via tools that emit hundreds of events per save.
5. **Auth.** Wrap `/api/commands/:verb` behind a shared secret header
(`X-VM-Token`) matching what the control plane injects when it dispatches.