Difficulty
10/10 — Expert. Estimated effort: 5–7 days for a senior engineer.
Context
The current room model requires clients to explicitly join_room with a known roomId (e.g., fleet-alpha). In large-scale deployments (100K+ assets across a continent), manually managing room memberships is infeasible. Assets move across regions, enter/exit depots, follow corridors — room membership should be automatic based on real-time position. The README.md describes "Room-Based Broadcasting — Clients join logical rooms (e.g., fleet ID, region, user group)" (line 32) but provides no mechanism for dynamic, position-based room assignment.
Problem statement
Design and implement a topology-aware room system that:
-
Geohash-based dynamic rooms: Divide the world into geohash cells (configurable precision, default 6 → ~1.2km × 0.6km). Each cell is an implicit room geo:{geohash}. When a client publishes a location, the server automatically adds them to the cell's room and removes them from the previous cell's room.
-
Hierarchical region aggregation: Parent rooms geo:{geohash_prefix}* aggregate child cells. A dispatcher subscribing to geo:dr5r* (precision 4, ~20km) receives all updates from child cells dr5ru, dr5rv, etc. Implementation: on broadcast to geo:dr5r*, fan out to all matching child rooms.
-
Proximity rooms: For a given client, automatically create ephemeral rooms prox:{clientId}:{radius}m containing all other clients within radius meters. Updated every N seconds (configurable). Used for collision avoidance, platooning, "vehicles near me" views.
-
Polygon-based rooms (geofence-linked): When a geofence (issue 12) is created with roomId, clients inside that fence are automatically members of that room. Entry/exit events trigger room join/leave.
-
Room membership as derived state: Explicit join_room/leave_room still work for logical rooms (fleet IDs, user groups). Dynamic rooms are additional memberships managed by the server. Client's getClientRooms() returns both explicit and dynamic rooms.
-
Efficient spatial indexing for room assignment: Use the same R-tree (issue 12) or a geohash trie to determine cell membership in O(log N). Batch updates: on each location update, compute new cell, if changed → leave(oldCell), join(newCell).
-
Configuration: TOPOLOGY_GEOHASH_PRECISION: 6, TOPOLOGY_PROXIMITY_RADIUS_M: 500, TOPOLOGY_PROXIMITY_UPDATE_MS: 5000, TOPOLOGY_ENABLE_DYNAMIC: true.
Current behavior
room-manager.js: only explicit join_room/leave_room. No automatic assignment.
server.js: location_update broadcasts to explicit rooms only.
- No geohash, proximity, or hierarchical rooms.
Required behavior
- New module
src/topology-manager.js exporting TopologyManager class.
TopologyManager constructor: { roomManager, geofenceEngine, config }.
topologyManager.processLocationUpdate(clientId, location) — computes geohash cell, updates dynamic memberships, updates proximity room.
topologyManager.getDynamicRooms(clientId) — returns Set of dynamic room IDs for a client.
topologyManager.getProximityMembers(clientId) — returns client IDs in proximity room.
topologyManager.subscribeToRegion(clientId, geohashPrefix) — subscribes client to hierarchical region room.
- Geohash encoding/decoding: use
ngeohash algorithm (no dependency — implement 20 lines).
- Hierarchical room broadcast:
roomManager.broadcast("geo:dr5r*", msg) → internally fans out to all geo:dr5ru*, geo:dr5rv*, etc. rooms.
- Proximity room: maintained as
Map<clientId, Set<clientId>> updated on timer. Broadcast to prox:{clientId}:500 sends to all members.
Constraints
- Do not modify
auth.js, validator.js, rate-limiter.js, conn-rate-limiter.js, logger.js, errors.js, room-manager.js, geofence-engine.js, protocol-registry.js, distributed-room-manager.js, tls-manager.js, admin-server.js, session-manager.js, compression.js.
- Do not modify existing test files. New test files required.
- No new npm dependencies — implement geohash encoding in ~30 lines.
- Dynamic room joins/leaves must not trigger
join_room/leave_room protocol messages to client (they're server-managed).
- Client's
getClientRooms() includes dynamic rooms — but client doesn't need to know which are dynamic vs explicit.
- Proximity room updates must be batched and debounced (5s default) to avoid churn.
- Memory: proximity rooms for 100K clients × avg 10 neighbors = 1M entries max.
Acceptance criteria
Out of scope
- Client-side geohash computation (server-only).
- Dynamic room persistence across restarts (recomputed on reconnect).
- Multi-level hierarchy beyond 2 levels (cell + prefix) — sufficient for this issue.
- Proximity room for non-location message types.
Hints and references
- Geohash encoding (base32, 5 bits per char):
const BASE32 = '0123456789bcdefghjkmnpqrstuvwxyz';
function encodeGeohash(lat, lon, precision = 6) {
let latMin = -90, latMax = 90, lonMin = -180, lonMax = 180;
let hash = '', bits = 0, bit = 0, evenBit = true;
while (hash.length < precision) {
if (evenBit) {
const mid = (lonMin + lonMax) / 2;
if (lon >= mid) { bit = (bit << 1) | 1; lonMin = mid; }
else { bit = (bit << 1) | 0; lonMax = mid; }
} else {
const mid = (latMin + latMax) / 2;
if (lat >= mid) { bit = (bit << 1) | 1; latMin = mid; }
else { bit = (bit << 1) | 0; latMax = mid; }
}
evenBit = !evenBit;
if (++bits === 5) { hash += BASE32[bit]; bits = 0; bit = 0; }
}
return hash;
}
- Hierarchical broadcast:
roomManager stores Map<roomId, Set<roomId>> of parent → children. On broadcast(parentPrefix + "*"), expand to children.
- Proximity: use R-tree from issue 12 (
rbush) with minX/maxX = lon ± radius/111km, minY/maxY = lat ± radius/111km for candidate filter, then exact haversine.
- Integration: in
server.js location_update case, after geofence processing, call topologyManager.processLocationUpdate(actualClientId, { lat, lon }).
Difficulty
10/10 — Expert. Estimated effort: 5–7 days for a senior engineer.
Context
The current room model requires clients to explicitly
join_roomwith a knownroomId(e.g.,fleet-alpha). In large-scale deployments (100K+ assets across a continent), manually managing room memberships is infeasible. Assets move across regions, enter/exit depots, follow corridors — room membership should be automatic based on real-time position. TheREADME.mddescribes "Room-Based Broadcasting — Clients join logical rooms (e.g., fleet ID, region, user group)" (line 32) but provides no mechanism for dynamic, position-based room assignment.Problem statement
Design and implement a topology-aware room system that:
Geohash-based dynamic rooms: Divide the world into geohash cells (configurable precision, default 6 → ~1.2km × 0.6km). Each cell is an implicit room
geo:{geohash}. When a client publishes a location, the server automatically adds them to the cell's room and removes them from the previous cell's room.Hierarchical region aggregation: Parent rooms
geo:{geohash_prefix}*aggregate child cells. A dispatcher subscribing togeo:dr5r*(precision 4, ~20km) receives all updates from child cellsdr5ru,dr5rv, etc. Implementation: onbroadcasttogeo:dr5r*, fan out to all matching child rooms.Proximity rooms: For a given client, automatically create ephemeral rooms
prox:{clientId}:{radius}mcontaining all other clients withinradiusmeters. Updated every N seconds (configurable). Used for collision avoidance, platooning, "vehicles near me" views.Polygon-based rooms (geofence-linked): When a geofence (issue 12) is created with
roomId, clients inside that fence are automatically members of that room. Entry/exit events trigger room join/leave.Room membership as derived state: Explicit
join_room/leave_roomstill work for logical rooms (fleet IDs, user groups). Dynamic rooms are additional memberships managed by the server. Client'sgetClientRooms()returns both explicit and dynamic rooms.Efficient spatial indexing for room assignment: Use the same R-tree (issue 12) or a geohash trie to determine cell membership in O(log N). Batch updates: on each location update, compute new cell, if changed →
leave(oldCell),join(newCell).Configuration:
TOPOLOGY_GEOHASH_PRECISION: 6,TOPOLOGY_PROXIMITY_RADIUS_M: 500,TOPOLOGY_PROXIMITY_UPDATE_MS: 5000,TOPOLOGY_ENABLE_DYNAMIC: true.Current behavior
room-manager.js: only explicitjoin_room/leave_room. No automatic assignment.server.js:location_updatebroadcasts to explicit rooms only.Required behavior
src/topology-manager.jsexportingTopologyManagerclass.TopologyManagerconstructor:{ roomManager, geofenceEngine, config }.topologyManager.processLocationUpdate(clientId, location)— computes geohash cell, updates dynamic memberships, updates proximity room.topologyManager.getDynamicRooms(clientId)— returns Set of dynamic room IDs for a client.topologyManager.getProximityMembers(clientId)— returns client IDs in proximity room.topologyManager.subscribeToRegion(clientId, geohashPrefix)— subscribes client to hierarchical region room.ngeohashalgorithm (no dependency — implement 20 lines).roomManager.broadcast("geo:dr5r*", msg)→ internally fans out to allgeo:dr5ru*,geo:dr5rv*, etc. rooms.Map<clientId, Set<clientId>>updated on timer. Broadcast toprox:{clientId}:500sends to all members.Constraints
auth.js,validator.js,rate-limiter.js,conn-rate-limiter.js,logger.js,errors.js,room-manager.js,geofence-engine.js,protocol-registry.js,distributed-room-manager.js,tls-manager.js,admin-server.js,session-manager.js,compression.js.join_room/leave_roomprotocol messages to client (they're server-managed).getClientRooms()includes dynamic rooms — but client doesn't need to know which are dynamic vs explicit.Acceptance criteria
geo:dr5reg(precision 6)geo:..., joins newgeo:...within 100msgeo:dr5r*→ receives updates from all child cellsprox:clientA:500contains clientB within 500mroomId: "depot-1"→ clients inside automatically in room "depot-1"getClientRooms(clientId)returns explicit + dynamic roomsnpm run lintpassestests/topology-manager.test.jswith geohash, hierarchical, proximity, geofence-room scenariosOut of scope
Hints and references
roomManagerstoresMap<roomId, Set<roomId>>of parent → children. Onbroadcast(parentPrefix + "*"), expand to children.rbush) withminX/maxX = lon ± radius/111km,minY/maxY = lat ± radius/111kmfor candidate filter, then exact haversine.server.jslocation_updatecase, after geofence processing, calltopologyManager.processLocationUpdate(actualClientId, { lat, lon }).