Store / mapStore
Source: src/store/mapStore.ts.
mapStore holds the canonical entity graph and is the only undoable store in the app. Every Apollo entity, every drawing primitive, and every overlap lives in its entities Map. Mutations flow through the license editable-guard, immer drafts, and zundo temporal middleware so undo / redo always rolls back to a consistent snapshot.
See /architecture/state-management for the R1 undo invariant (CANCEL before temporal.undo()).
State Shape
interface MapState {
entities: Map<string, MapEntity>;
}2
3
MapEntity is a discriminated union over entityType covering Apollo entities (lane, road, junction, crosswalk, signal, stopSign, yieldSign, speedBump, clearArea, parkingSpace, pncJunction, rsu, area, barrierGate, overlap) plus editor-only drawing primitives (polyline, catmullRom, bezier, arc, rect, polygon).
Actions
interface MapActions {
addEntity(entity: MapEntity): void;
updateEntity(id: string, entity: MapEntity): void;
removeEntity(id: string): void;
reparentEntity(childId: string, target: ParentTarget): ReparentResult;
batchImport(entities: MapEntity[]): void;
replaceImportedEntities(entities: MapEntity[]): void;
replaceImportedEntityMap(entities: Map<string, MapEntity>): void;
recomputeOverlapsAsync(): Promise<{
pairsTested: number;
pairsMatched: number;
overlapsCreated: number;
overlapsRemoved: number;
durationMs: number;
} | null>;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Single-entity mutations
addEntity and updateEntity run inside one immer draft:
assertEditable(action)short-circuits on read-only.- Write the entity to
state.entities. - If
entityTypeis topology-affecting (lane/junction), runreconcileLaneTopologyIncrementaland merge changes. - Run
applyOverlapPatch(incremental) over the accumulated dirty set.
updateEntity additionally captures the previous entity and passes it as previousEntities so the topology reconciler can compare pred / succ before / after.
removeEntity(id)
The most complex mutator:
- Editable guard.
- Capture removed entity reference and bbox.
- Use
getSharedSpatialIndex()to collect spatial-neighbour lanes (geometry-derived overlaps that don't reference the removed id still need reconcile coverage). - Run
cascadeDeleteRefsFullfor direct reference cleanup (changes) and orphaned-overlap detection (cascadeRemoved). - Inside one immer producer: apply cleanups, delete cascade orphans, delete the original, run topology + overlap reconcile.
- If the removed entity was a lane, call
invalidateLaneCaches.
reparentEntity(childId, target)
Wraps reparent from @/lib/entityOps. Returns ReparentResult so callers can surface UX feedback. No topology / overlap reconcile — reparent only touches junctionId / road.section.laneIds.
batchImport(entities)
Single-transaction bulk loader: write everything, run full topology reconcile, then full overlap reconcile. Avoids the accumulated drift of N incremental rounds.
replaceImportedEntities / replaceImportedEntityMap
replaceImportedEntityMap(entities) {
const t = useMapStore.temporal.getState();
t.pause();
try {
set({ entities });
t.clear();
} finally {
t.resume();
}
resetSharedSpatialIndex();
}2
3
4
5
6
7
8
9
10
11
Pause + clear undo so the post-import state is the new "initial". The shared spatial index is reset because the previous session's entries are stale.
recomputeOverlapsAsync()
Off-thread overlap rebuild via OverlapWorkerBridge. Result patches land in a single zundo transaction; the shared spatial index is reset afterwards.
zundo Middleware
temporal(immer(...), {
partialize: (state) => ({ entities: state.entities }),
limit: readHistoryLimit(),
});2
3
4
partializekeeps onlyentitiesin history (action methods are never snapshotted).limitis read once at module-load fromsettingsStore. Changes apply on next reload.
useMapStore.temporal.getState() exposes undo, redo, pause, resume, clear, plus pastStates / futureStates.
Examples
// Add an entity
useMapStore.getState().addEntity({ id: 'lane_1', entityType: 'lane' /* ... */ });
// Subscribe in a component
const count = useMapStore((s) => s.entities.size);
// Undo / redo
useMapStore.temporal.getState().undo();
// Recompute overlaps off-thread
const stats = await useMapStore.getState().recomputeOverlapsAsync();2
3
4
5
6
7
8
9
10
11
Related
- /architecture/state-management
- /api/lib/editable-guard
- /api/lib/entity-ops
- /api/store/settings-store —
historyLimitsource. - /api/store/ui-store — non-undoable UX state.