Skip to main content

Collab Server (server/)

Lightweight Node.js WebSocket server for realtime collaboration. Manages project rooms, syncs file changes between connected Songbird clients, and performs structured merges on .bird and JSON files.

Architecture

WebSocket Clients (Songbird instances)


┌──────────────────────────────────┐
│  server.js                        │
│  ├─ Connection handler            │
│  ├─ Room manager                  │
│  │   ├─ create (→ invite code)    │
│  │   ├─ join (← current files)    │
│  │   └─ leave (cleanup)           │
│  ├─ Push handler                  │
│  │   ├─ mergeFile()               │
│  │   └─ broadcast()               │
│  └─ Presence relay                │
│                                    │
│  merge/                            │
│  ├─ bird-merge.js                  │
│  │   ├─ parseBird()               │
│  │   ├─ serializeBird()           │
│  │   └─ mergeBird()               │
│  └─ json-merge.js                  │
│      ├─ mergeJSON()               │
│      ├─ mergeStateJson()          │
│      └─ mergeEditJson()           │
└──────────────────────────────────┘

Files

FilePurpose
server.jsMain WebSocket server — room management, push/pull sync, presence broadcasting, merge orchestration
merge/bird-merge.jsStructured .bird file parser (parseBird), serializer (serializeBird), and three-way merge (mergeBird) at track-per-section granularity
merge/json-merge.jsRecursive deep-merge for JSON state files. Handles arrays element-by-element, objects key-by-key. mergeStateJson() for mixer state, mergeEditJson() for plugin state
test-merge.jsTest suite — 7 tests covering parse round-trip, auto-merge of different tracks/sections, and last-write-wins conflict resolution
package.jsonDependencies: ws (WebSocket), nanoid (invite codes)

Room Management

Each project collaboration session is a room:
  • Create — A user creates a room, receives an 8-character invite code. Their current project files are sent to the server as the initial state.
  • Join — A user joins with an invite code and receives the current project files from the server.
  • Leave — On disconnect, peers are notified. Empty rooms are cleaned up after 60 seconds.
Rooms are stored in-memory. No database is needed — if the server restarts, rooms are lost (clients can recreate them).

Merge Engine

.bird Three-Way Merge

The .bird merge works by parsing the file into a structured tree and merging at the track-per-section level:
const tree = parseBird(birdText);
// tree = {
//   sig: "sig\n  bpm 128\n  scale A aeolian",
//   tracks: "tracks\n  1 kick\n    plugin kick\n  ...",
//   arr: "arr\n  intro 4\n  verse 8",
//   sections: Map {
//     "intro" => Map { "1_kick" => "  trk 1 kick\n    p q _ q _\n    ...", ... },
//     "verse" => Map { "1_kick" => "...", "2_bass" => "...", ... }
//   },
//   sectionOrder: ["intro", "verse"]
// }
Three-way merge logic:
  1. Parse base, ours, theirs into trees
  2. For each block (sig, tracks, arr): if theirs changed from base, take theirs; else keep ours
  3. For each section → each track: if only one side changed, take the change. If both changed the same track in the same section → last-write-wins (take theirs)
  4. Serialize the merged tree back to .bird text

JSON Deep Merge

Recursive merge at every level of the JSON tree:
  • Objects: merge key-by-key recursively
  • Arrays: merge element-by-element by index (critical for the tracks array in daw.state.json)
  • Leaves: if both changed, last-write-wins (take theirs)

Running

# Development (auto-restart on changes)
npm run dev

# Production
npm start

# Custom port
PORT=9000 npm start

# Run tests
node test-merge.js

Extending

Adding New File Types

To add merge support for a new file type:
  1. Create a new merge module in merge/ (e.g., merge/new-format-merge.js)
  2. Export a mergeNewFormat(base, ours, theirs) function
  3. Add a file extension check in server.js’s mergeFile() function
  4. Unknown file types already fall back to last-write-wins